From c3112c973ca95636804df876ce331405f1b437eb Mon Sep 17 00:00:00 2001 From: DrakRosmann Date: Thu, 24 Sep 2026 18:31:44 -0300 Subject: [PATCH 1/4] =?UTF-8?q?Implementa=C3=A7=C3=A3o=20da=20simula=C3=A7?= =?UTF-8?q?=C3=A3o=20em=20commsui=20e=20taskmap.=20[feature]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- documentation/implementacoes.md | 119 +++++++++++++++++++++++++++-- main.py | 129 ++++++++++++++------------------ pe_matrix.py | 46 ++++++++++++ slave_matrix.py | 29 +++++-- util/MPSoCConfig.py | 9 ++- 5 files changed, 245 insertions(+), 87 deletions(-) diff --git a/documentation/implementacoes.md b/documentation/implementacoes.md index 1779347..844d72e 100644 --- a/documentation/implementacoes.md +++ b/documentation/implementacoes.md @@ -13,9 +13,10 @@ Este documento descreve as funcionalidades portadas do [GraphicalDebugger origin 7. [Matriz de roteadores — `router_matrix.py`](#7-matriz-de-roteadores--router_matrixpy) 8. [Simulação — `simulation.py`](#8-simulação--simulationpy) 9. [Interface da simulação — `main.py`](#9-interface-da-simulação--mainpy) -10. [Diferenças e correções em relação ao Java](#10-diferenças-e-correções-em-relação-ao-java) -11. [Limitações e próximos passos](#11-limitações-e-próximos-passos) -12. [Como testar](#12-como-testar) +10. [Communication Overview, Task Mapping e Message Log — `overview_windows.py`](#10-communication-overview-task-mapping-e-message-log--overview_windowspy) +11. [Diferenças e correções em relação ao Java](#11-diferenças-e-correções-em-relação-ao-java) +12. [Limitações e próximos passos](#12-limitações-e-próximos-passos) +13. [Como testar](#13-como-testar) --- @@ -41,6 +42,9 @@ diretório debug/ |---|---|---| | `main.py` | Janela principal: menus, botões, tabelas, ligação com a simulação | `MainFrame.java` | | `simulation.py` | Controle da simulação e janelas de checkpoint | `MainFrame.java` (parte de simulação), `CheckpointController.java` | +| `overview_windows.py` | Janelas Communication Overview, Task Mapping Overview e Message Log, atualizadas durante a simulação | `CommunicationOverview.java`, `TaskMappingFrame.java`, `MainFrame.showMessageList` | +| `slave_matrix.py` / `pe_matrix.py` | Células das grades das duas janelas acima | — | +| `deloream.py` | Deloream: leitor das mensagens `$$$` das tarefas nos logs dos processadores | `deloream/DeloreamMainFrame.java`, `deloream/TaskMessage.java` | | `theme.py` | Paletas dos temas claro/escuro, cores da grade e preferência salva | — | | `router_matrix.py` | Desenho da grade e de cada roteador (imagens, setas, %) | `Roteador.java`, `UJPanelImagem.java`, `MainFrame.createNoCPanel` | | `util/MPSoCConfig.py` | Leitura de `platform.cfg` e `services.cfg`, constantes e endereçamento | `util/MPSoCConfig.java` | @@ -346,7 +350,7 @@ Divide o tempo em janelas de **0,5 ms** (`window_size_ms`). `set_time(ticks)` co - `theme.py` define as paletas do Fusion para os dois temas, incluindo as cores de itens **desabilitados** e do texto de *placeholder* (sem elas, botões desabilitados ficavam ilegíveis no modo escuro). - `MinhaJanela.set_dark_mode(dark)` aplica a paleta na aplicação inteira, atualiza a marcação do menu e recolore a grade (`RouterMatrixWidget.apply_theme`). Uma grade criada depois (novo debug ou reset) já nasce com o tema atual. - A escolha é salva com `QSettings` (organização `GAPH`, aplicação `MemphisGraphicalDebugger`) e restaurada ao abrir o programa. -- As janelas Communication Overview e Task Mapping mantêm suas cores fixas. +- As células das janelas Communication Overview e Task Mapping mantêm suas cores próprias (mapa de calor e cores das aplicações); o restante da janela segue o tema. ### 9.3 Mensagens @@ -359,7 +363,97 @@ Divide o tempo em janelas de **0,5 ms** (`window_size_ms`). `set_time(ticks)` co --- -## 10. Diferenças e correções em relação ao Java +## 10. Communication Overview, Task Mapping e Message Log — `overview_windows.py` + +**Menus:** `Tools → Communication Overview`, `Tools → Task Mapping Overview` e `Tools → Message Log` + +As três janelas mostram o estado da simulação **até o tempo atual** e se atualizam sozinhas enquanto ela avança (play, `>||`, **Back To**). + +### 10.1 Atualização durante a simulação (`_LiveOverview`) + +- A janela se conecta ao sinal `time_changed` do `SimulationController` e só **marca** que os dados mudaram. +- Um `QTimer` redesenha no máximo a cada **500 ms** (`REFRESH_INTERVAL_MS`), e só se a janela estiver visível e houver mudança. Assim o modo rápido (velocidade 99–100) não é travado pelas janelas abertas. No Java era uma `Thread` com `sleep(1000)`. +- Abrir o menu de novo com a janela já aberta só a traz para frente (não cria outra). +- **New Debugging** e **Reset Simulation** chamam `set_information(...)` nas janelas abertas: elas recriam a grade e passam a seguir o novo debug/controlador. + +### 10.2 Communication Overview (`CommunicationOverviewWindow`) + +Mapa de calor do tráfego acumulado em cada roteador. + +| Controle | Efeito | +|---|---| +| **All Services** (padrão) | Considera todos os serviços; desabilita os filtros abaixo | +| Lista de serviços | Considera só o serviço escolhido | +| **Global** | % = tráfego do serviço no roteador / tráfego do serviço em toda a NoC | +| **All traffic** | % = tráfego do serviço no roteador / todo o tráfego da NoC | +| **Router** | % = tráfego do serviço no roteador / todo o tráfego do próprio roteador | +| **Volume** (padrão) / **Bandwidth** | Mede em flits (`size + 1`) ou em ciclos de enlace | + +- Cada célula mostra o tipo do PE (`Global M`, `Cluster M`, `Slave`), o rótulo do roteador, a porcentagem com 3 casas e a unidade (`flits`/`cycles`). +- Cor: mesma escala do Java, matiz HSB de `0.65` (azul, 0%) até `0` (vermelho, 100%), saturação 1 e brilho 0.8. A barra **Color Length** mostra a escala. +- **Statistics:** roteador com a maior porcentagem (**Bigger**), média entre todos os roteadores (**Average**) e roteador com a menor (**Small**). +- O fundo da célula é desenhado em `SlaveWidget.paintEvent` (trocar a folha de estilo do próprio widget não repintava o fundo). + +### 10.3 Task Mapping Overview (`TaskMappingWindow`) + +Tarefas de cada PE, montadas a partir dos eventos de `RouterInformation.get_tasks_information()`: + +- `ALLOCATED` → **RUN**, `TERMINATED` → **TER** (vale o último evento de cada tarefa). +- Cada tarefa aparece como `nome ID STATUS`; o nome vem do `platform.cfg` (`BEGIN_task_name_relation`) ou, se não existir, `app tarefa`. +- Cor por aplicação (`task_id >> 8`), sorteada com semente fixa (20, como no Java), então cada aplicação mantém a cor. O texto fica preto ou branco conforme a claridade do fundo. + +| Controle | Efeito | +|---|---| +| **All tasks status** | Todas as tarefas | +| **Only Running** (padrão) | Só as que estão rodando | +| **Only Terminated** | Só as terminadas (o `.ui` repetia "All tasks status" neste botão) | +| **Updating / Update** | Ligado: atualiza sozinho. Desligado: congela a visão; ao religar, atualiza na hora | +| **Without Task ID** | Esconde o ID numérico (no Java a opção existia, mas o botão não atualizava a tela) | + +Se o `platform.cfg` não tiver nomes de tarefas, a janela avisa uma vez que os IDs serão mostrados como números. + +### 10.4 Message Log (`MessageLogWindow`) + +Lista (500×600, como no original) das mensagens entre PEs, na ordem do `traffic_router.txt`: + +| Condição do pacote | Linha | +|---|---| +| Entrou pela porta `LOCAL0`/`LOCAL1` (saindo do PE) | `---->>> send message to service ` | +| `router_address == target_router` (chegou ao destino) | `<<<---- receive message with service ` | + +Se as duas condições valem (mensagem para o próprio PE), fica a linha de recebimento, como no Java. + +- Mostra todos os pacotes **já lidos** e acrescenta os novos conforme a simulação avança. Voltar no tempo não apaga linhas, pois os pacotes continuam lidos (mesmo comportamento do original). +- A lista acompanha o fim automaticamente, a menos que o usuário tenha rolado para cima. +- Os roteadores aparecem como `XxY` com endereçamento XY (o Java mostrava o endereço hamiltoniano numérico) ou pelo número com endereçamento HAMILTONIAN, igual ao resto da interface. + +### 10.5 Deloream (`deloream.py`) + +**Menu:** `Tools → Deloream` — *Debug Log Reader for MPSoCs*. Janela independente (800×700) que lê a pasta do cenário, a que contém o diretório `debug/` aberto (`MPSoCConfig.get_testcase_path()`): + +``` +/ + ├─ debug/platform.cfg nomes das tarefas (BEGIN_task_name_relation) e das aplicações (BEGIN_app_name_relation) + └─ log/logx.txt saída de cada processador +``` + +- **Árvore** `Applications → aplicação[id] → tarefa[id]`; a tarefa pertence à aplicação `id >> 8`. +- **Duplo clique** (ou Enter) numa tarefa: procura em todos os arquivos de `log/` as linhas no formato + + ``` + $$$____ + ``` + + com a aplicação e a tarefa escolhidas, e mostra as mensagens agrupadas por processador (`-- Processor: 1x1`), na ordem dos arquivos e das linhas. +- Sem mensagens (ou sem a pasta `log/`): *"Log file not created yet or no message found!"* em vermelho. +- **Options → New testcase** (Ctrl+N) abre outro cenário; **Reload** (F5) relê o `platform.cfg`; **Exit** (Ctrl+E) fecha só a janela. +- Abrir o menu de novo reaproveita a janela; se o debug carregado for de outro cenário, a árvore é recarregada. + +Classes/funções: `TaskMessage.parse(line)`, `read_name_relations(platform_path)`, `read_task_messages(log_dir, app_id, task_id)` e `DeloreamWindow`. + +--- + +## 11. Diferenças e correções em relação ao Java | Local | Java | Python | |---|---|---| @@ -375,20 +469,28 @@ Divide o tempo em janelas de **0,5 ms** (`window_size_ms`). `set_time(ticks)` co | Thread de simulação | `Thread` + `sleep()` alterando a interface | `QTimer` na thread principal | | Getters/setters | Métodos `getX()`/`setX()` | Atributos e `dataclass` | | `TaskInformation.remote_task_id` | `0` quando ausente | `-1` quando ausente | +| Task Mapping: atualização | `Thread` recriando todos os painéis a cada 1 s | `QTimer` que só redesenha se chegou pacote novo, reaproveitando os widgets | +| Task Mapping: ordem das tarefas | Ordem do `HashMap` (aleatória) | Ordenadas pelo ID | +| Task Mapping: cor do texto | Sempre preto (ilegível em cores escuras) | Preto ou branco conforme o fundo | +| Communication Overview: atualização | Calculado só ao abrir ou ao mudar um filtro | Acompanha a simulação | +| Deloream: mensagem | Juntava as partes sem os `_` (`valor_de_x` virava `valordex`) | Mantém a mensagem inteira | +| Deloream: busca | Procurava `_app_tarefa_` em qualquer ponto da linha (podia casar com o texto da mensagem) | Compara os campos de aplicação e tarefa | +| Deloream: Exit / New testcase | `Exit` fechava o debugger inteiro (`System.exit`); `New testcase` lia um caminho `null` | Fecha só a janela; abre um seletor de pasta | +| `get_testcase_path` | Cortava o caminho na primeira ocorrência de "debug" (quebrava em pastas como `debugger/`) | Pasta pai do diretório `debug` | +| Message Log | Retrato dos pacotes lidos no momento em que a janela abria; endereço hamiltoniano | Acrescenta as mensagens novas durante a simulação; endereço `XxY` com endereçamento XY | --- -## 11. Limitações e próximos passos +## 12. Limitações e próximos passos - **Back To** só vai até tempos já simulados (mesmo comportamento do Java). - **Filtro de serviços/PEs** (`Filters → Service and PE Filter`): ainda não implementado. O controlador já aceita `SimulationController.packet_filter`. - **Janela de informações do roteador** (`RouterInfoFrame`): o `RouterWidget` já emite `clicked(router_address)`, falta a janela. -- **Communication Overview**, **Task Mapping Overview** e **Message Log** ainda não usam os dados do `MPSoCInformation`. - O relatório de uso de link por roteador (`printRouterTotalLinkUsage`, oculto no Java) não foi portado. --- -## 12. Como testar +## 13. Como testar 1. Extraia o `DEBUG EXAMPLE.zip`. 2. Execute `uv run main.py` (ou `.venv/bin/python main.py`). @@ -396,5 +498,6 @@ Divide o tempo em janelas de **0,5 ms** (`window_size_ms`). `set_time(ticks)` co 4. Use `>||` para avançar pacote a pacote, `>` para rodar e o slider para a velocidade (99–100 = modo rápido). 5. Digite um tempo já simulado em **Back To** e clique em **Go**. 6. `File → Reset Simulation` volta ao início. +7. Abra `Tools → Communication Overview`, `Tools → Task Mapping Overview` e `Tools → Message Log` e rode a simulação: as porcentagens, as tarefas e as mensagens mudam conforme os pacotes avançam (2 755 mensagens ao fim do exemplo). Resultados esperados com o exemplo: 3 101 pacotes, último pacote em 6 251 176 ticks (62,51176 ms com clock de 10 ns), 42 serviços e 11 tarefas. diff --git a/main.py b/main.py index fb349f0..8063286 100644 --- a/main.py +++ b/main.py @@ -8,14 +8,12 @@ import theme from ui_mainwindow import MainWindow -from commsUi import Ui_Form -from taskmap import Ui_TaskMap from util import MPSoCConfig from information import MPSoCInformation from router_matrix import RouterMatrixWidget from simulation import SimulationController, MAX_REPAINT_SPEED, ticks_to_ms -from slave_matrix import SlaveMatrixWidget -from pe_matrix import PEMatrixWidget +from overview_windows import CommunicationOverviewWindow, MessageLogWindow, TaskMappingWindow +from deloream import DeloreamWindow class MinhaJanela(QMainWindow, MainWindow): @@ -25,6 +23,10 @@ class MinhaJanela(QMainWindow, MainWindow): router_matrix_widget = None frame_scroll_area = None simulation = None + communication_window = None + task_mapping_window = None + message_log_window = None + deloream_window = None # Arquivos obrigatórios em um diretório de debug REQUIRED_FILES = ("platform.cfg", "services.cfg", "traffic_router.txt") @@ -67,6 +69,8 @@ def __init__(self): self.actionTask_Mapping_Overview.triggered.connect(self.open_taskmap) self.actionTask_List.triggered.connect(self.taskList) self.actionServices_List.triggered.connect(self.servicesList) + self.actionMessage_Log.triggered.connect(self.open_message_log) + self.actionDeloream.triggered.connect(self.open_deloream) # --------------------------------------------------------- # CONTROLE DA SIMULAÇÃO @@ -124,15 +128,56 @@ def open_communication(self): if self.filePath == "": QMessageBox.warning(self, "Attention", "Please, load a debugging before") return - self.janela_secundaria = NovaJanela(self.mpconfig) - self.janela_secundaria.show() + # Reaproveita a janela se já estiver aberta + if self.communication_window is None or not self.communication_window.isVisible(): + self.communication_window = CommunicationOverviewWindow( + self.mpconfig, self.mpsoc_information, self.simulation) + self.communication_window.show() + self.communication_window.raise_() + self.communication_window.activateWindow() + + def open_deloream(self): + if self.filePath == "": + QMessageBox.warning(self, "Attention", "Please, load a debugging before") + return + + testcase_path = self.mpconfig.get_testcase_path() + if testcase_path is None: + QMessageBox.warning(self, "Deloream", + "The debug directory must be named \"debug\" and be inside the " + "testcase folder (the one that contains the log/ folder).") + return + + # Mesma janela enquanto estiver aberta; outro cenário recarrega a árvore + if self.deloream_window is None or not self.deloream_window.isVisible(): + self.deloream_window = DeloreamWindow(testcase_path) + elif self.deloream_window.testcase_path != testcase_path: + self.deloream_window.load_testcase(testcase_path) + self.deloream_window.show() + self.deloream_window.raise_() + self.deloream_window.activateWindow() + + def open_message_log(self): + if self.filePath == "": + QMessageBox.warning(self, "Attention", "Please, load a debugging before") + return + if self.message_log_window is None or not self.message_log_window.isVisible(): + self.message_log_window = MessageLogWindow( + self.mpconfig, self.mpsoc_information, self.simulation) + self.message_log_window.show() + self.message_log_window.raise_() + self.message_log_window.activateWindow() def open_taskmap(self): if self.filePath == "": QMessageBox.warning(self, "Attention", "Please, load a debugging before") return - self.janela_taskmap = TaskMapWindow(self.mpconfig) - self.janela_taskmap.show() + if self.task_mapping_window is None or not self.task_mapping_window.isVisible(): + self.task_mapping_window = TaskMappingWindow( + self.mpconfig, self.mpsoc_information, self.simulation) + self.task_mapping_window.show() + self.task_mapping_window.raise_() + self.task_mapping_window.activateWindow() def update_slide(self, valor): self.label_2.setText(str(valor)) @@ -234,6 +279,11 @@ def start_simulation(self): self.simulation = SimulationController(self.mpconfig, self.mpsoc_information, self.router_matrix_widget, self) self.simulation.set_speed(self.horizontalSlider.value()) + + # Janelas de visão geral abertas passam a acompanhar o novo debug + for window in (self.communication_window, self.task_mapping_window, self.message_log_window): + if window is not None and window.isVisible(): + window.set_information(self.mpconfig, self.mpsoc_information, self.simulation) self.simulation.time_changed.connect(self.update_simulation_time) self.simulation.packet_changed.connect(self.update_current_packet_table) self.simulation.running_changed.connect(self.on_simulation_running_changed) @@ -417,69 +467,6 @@ def create_table_window(self, title, headers, rows): return table_frame -class NovaJanela(QWidget, Ui_Form): - def __init__(self, mpconfig=None): - super().__init__() - self.setupUi(self) - self.mpconfig = mpconfig - self.checkBox.checkStateChanged.connect(self.on_checkbox_changed) - - self.build_slave_matrix() - - def build_slave_matrix(self): - """ - Monta a matriz de slaves dentro do scrollArea gerado por commsUi.py - (self.scrollAreaWidgetContents), usando mpsoc_x/mpsoc_y do arquivo - de configuração atual — igual ao print do Communication Overview. - """ - if self.mpconfig is None: - return - - matrix = SlaveMatrixWidget(self.mpconfig.mpsoc_x, self.mpconfig.mpsoc_y) - - layout = QVBoxLayout(self.scrollAreaWidgetContents) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(matrix) - self.scrollAreaWidgetContents.setLayout(layout) - - def on_checkbox_changed(self): - if (self.checkBox.isChecked()): - self.radioButton.setEnabled(False) - self.radioButton_2.setEnabled(False) - self.radioButton_3.setEnabled(False) - self.comboBox.setEnabled(False) - else: - self.radioButton.setEnabled(True) - self.radioButton_2.setEnabled(True) - self.radioButton_3.setEnabled(True) - self.comboBox.setEnabled(True) - - -class TaskMapWindow(QWidget, Ui_TaskMap): - def __init__(self, mpconfig=None): - super().__init__() - self.setupUi(self) - self.mpconfig = mpconfig - - self.build_pe_matrix() - - def build_pe_matrix(self): - """ - Monta a matriz de PEs dentro do scrollArea gerado por taskmap.py - (self.scrollAreaWidgetContents), usando mpsoc_x/mpsoc_y do arquivo - de configuração atual — igual ao print do Task Mapping Overview. - """ - if self.mpconfig is None: - return - - matrix = PEMatrixWidget(self.mpconfig.mpsoc_x, self.mpconfig.mpsoc_y) - - layout = QVBoxLayout(self.scrollAreaWidgetContents) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(matrix) - self.scrollAreaWidgetContents.setLayout(layout) - - if __name__ == "__main__": app = QApplication(sys.argv) diff --git a/pe_matrix.py b/pe_matrix.py index 012dd65..c42993a 100644 --- a/pe_matrix.py +++ b/pe_matrix.py @@ -17,6 +17,7 @@ """ from PySide6.QtCore import Qt +from PySide6.QtGui import QColor from PySide6.QtWidgets import QGridLayout, QLabel, QSizePolicy, QVBoxLayout, QWidget @@ -32,8 +33,20 @@ background-color: transparent; color: #000000; } +QLabel#peTitle { + font-weight: bold; +} """ +# Uma linha de tarefa: fundo com a cor da aplicação +_TASK_STYLESHEET = "QLabel {{ background-color: {background}; color: {text}; font-weight: bold; padding: 2px; }}" + + +def _text_color_for(background: QColor) -> str: + """Texto preto em fundos claros e branco em fundos escuros.""" + luminance = 0.299 * background.red() + 0.587 * background.green() + 0.114 * background.blue() + return "#000000" if luminance > 140 else "#ffffff" + class PEWidget(QWidget): """ @@ -62,13 +75,46 @@ def __init__(self, pos_x: int, pos_y: int, parent=None): layout = QVBoxLayout(self) layout.setContentsMargins(4, 4, 4, 4) + layout.setSpacing(2) self.pe_label = QLabel(f"PE{pos_x}x{pos_y}") + self.pe_label.setObjectName("peTitle") self.pe_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(self.pe_label) + # Linhas de tarefas (preenchidas por set_tasks), centralizadas na célula + layout.addStretch(1) + self.tasks_layout = QVBoxLayout() + self.tasks_layout.setSpacing(2) + layout.addLayout(self.tasks_layout) + layout.addStretch(1) + + self.task_labels: list[QLabel] = [] + self.setLayout(layout) + def set_tasks(self, title: str, tasks: list[tuple[str, QColor]]) -> None: + """ + Mostra o título do PE e uma linha por tarefa, cada uma com o texto + e a cor da aplicação a que pertence. + """ + self.pe_label.setText(title) + + # Reaproveita os rótulos existentes e cria/remove só a diferença + while len(self.task_labels) < len(tasks): + label = QLabel(self) + label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.tasks_layout.addWidget(label) + self.task_labels.append(label) + while len(self.task_labels) > len(tasks): + self.task_labels.pop().deleteLater() + + for label, (text, color) in zip(self.task_labels, tasks): + label.setText(text) + style = _TASK_STYLESHEET.format(background=color.name(), text=_text_color_for(color)) + if label.styleSheet() != style: + label.setStyleSheet(style) + def set_task(self, task_name: str | None) -> None: """ Atualiza o rótulo para mostrar a tarefa alocada neste PE, além da diff --git a/slave_matrix.py b/slave_matrix.py index be63f6e..2680159 100644 --- a/slave_matrix.py +++ b/slave_matrix.py @@ -18,17 +18,16 @@ """ from PySide6.QtCore import Qt +from PySide6.QtGui import QColor, QPainter, QPen from PySide6.QtWidgets import QGridLayout, QLabel, QSizePolicy, QVBoxLayout, QWidget # Cores fixas, independentes do tema (claro/escuro) do MainWindow: sem # isso, o toggle Dark/Light recoloriria essas células via # QApplication.setPalette(). +# O fundo (cor do mapa de calor) é pintado em paintEvent, pois muda a +# cada atualização da simulação. _SLAVE_STYLESHEET = """ -QWidget#slaveWidget { - background-color: #0000cc; - border: 1px solid #ffffff; -} QLabel { background-color: transparent; color: #ffffff; @@ -56,8 +55,8 @@ def __init__(self, pos_x: int, pos_y: int, parent=None): # Isola a célula do tema global do app (ver comentário de # _SLAVE_STYLESHEET acima). - self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) self.setStyleSheet(_SLAVE_STYLESHEET) + self._background = QColor("#0000cc") # Cresce para preencher toda a área do scrollArea, como no print # (diferente do RouterWidget, que tem tamanho fixo 170x170). @@ -89,6 +88,26 @@ def __init__(self, pos_x: int, pos_y: int, parent=None): self.setLayout(layout) + def _set_background(self, color: QColor) -> None: + if color != self._background: + self._background = QColor(color) + self.update() + + def paintEvent(self, event): + # Fundo com a cor do mapa de calor e borda preta de 1 px + painter = QPainter(self) + painter.fillRect(self.rect(), self._background) + painter.setPen(QPen(QColor("#000000"), 1)) + painter.drawRect(self.rect().adjusted(0, 0, -1, -1)) + painter.end() + + def set_values(self, title: str, value: str, unit: str, background: QColor) -> None: + """Atualiza a célula com os dados do mapa de calor do Communication Overview.""" + self.title_label.setText(title) + self.percent_label.setText(value) + self.flits_label.setText(unit) + self._set_background(background) + def set_traffic(self, percent: float, flits: int | None = None) -> None: """ Atualiza o percentual mostrado (e, opcionalmente, a contagem de diff --git a/util/MPSoCConfig.py b/util/MPSoCConfig.py index 406b1b7..0a0607b 100644 --- a/util/MPSoCConfig.py +++ b/util/MPSoCConfig.py @@ -1,3 +1,5 @@ +import os + from sortedcontainers import SortedDict @@ -277,10 +279,11 @@ def get_pe_number(self): return self.mpsoc_x * self.mpsoc_y def get_testcase_path(self): - index = self.debug_file_path.find("debug") - if index < 1: + """Pasta do cenário: a que contém o diretório de debug (e o log/).""" + debug_dir = os.path.normpath(self.debug_file_path) + if os.path.basename(debug_dir) != "debug": return None - return self.debug_file_path[:index - 1] + return os.path.dirname(debug_dir) def get_frequency_in_hz(self): return 1_000_000_000 // self.clock_period_in_ns From a72ee030b594e97f23ce17b7104ed44297ff9d33 Mon Sep 17 00:00:00 2001 From: DrakRosmann Date: Thu, 24 Sep 2026 18:37:56 -0300 Subject: [PATCH 2/4] =?UTF-8?q?Implementa=C3=A7=C3=A3o=20de=20deloream.=20?= =?UTF-8?q?[feature]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + deloream.py | 260 ++++++++++++++++++ information/__init__.py | 25 ++ information/mpsoc_information.py | 82 ++++++ information/packet_information.py | 42 +++ information/port_information.py | 40 +++ information/read_traffic_data.py | 86 ++++++ information/router_information.py | 101 +++++++ information/router_neighbors.py | 59 ++++ information/task_information.py | 20 ++ overview_windows.py | 439 ++++++++++++++++++++++++++++++ simulation.py | 282 +++++++++++++++++++ theme.py | 91 +++++++ 13 files changed, 1530 insertions(+) create mode 100644 deloream.py create mode 100644 information/__init__.py create mode 100644 information/mpsoc_information.py create mode 100644 information/packet_information.py create mode 100644 information/port_information.py create mode 100644 information/read_traffic_data.py create mode 100644 information/router_information.py create mode 100644 information/router_neighbors.py create mode 100644 information/task_information.py create mode 100644 overview_windows.py create mode 100644 simulation.py create mode 100644 theme.py diff --git a/.gitignore b/.gitignore index 00f2d38..e2dbd4d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ __pycache__/ *.pyc .venv/ +build/ +dist/ +*.spec diff --git a/deloream.py b/deloream.py new file mode 100644 index 0000000..4e2df8c --- /dev/null +++ b/deloream.py @@ -0,0 +1,260 @@ +""" +Deloream — leitor dos logs de depuração das tarefas (Debug Log Reader for MPSoCs). + +Refatoração do pacote "deloream" (DeloreamMainFrame.java e TaskMessage.java) +do GraphicalDebugger original. + +Estrutura esperada de um cenário (testcase) do Memphis-V: + + / + ├─ debug/platform.cfg nomes das tarefas e das aplicações + └─ log/logx.txt saída de cada processador + +As mensagens de depuração das tarefas nos logs têm o formato + + $$$____ + +ex.: "$$$_1x1_3_1_Inicio da aplicacao prod" (tarefa 1 da aplicação 3 no PE 1x1). +""" + +import html +import os +import re +from dataclasses import dataclass + +from PySide6.QtCore import Qt +from PySide6.QtGui import QAction, QFont, QIcon, QKeySequence +from PySide6.QtWidgets import (QFileDialog, QMainWindow, QMessageBox, QSplitter, QTextBrowser, + QTreeWidget, QTreeWidgetItem) + +ICON_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icon", "delorean.png") + +# Marca das mensagens de depuração das tarefas nos logs +MESSAGE_MARK = "$$$" + +# $$$____; a mensagem pode conter "_" +_MESSAGE_PATTERN = re.compile(r"^\$\$\$_(?P[^_]+)_(?P\d+)_(?P\d+)_(?P.*)$") + +# Papel dos itens da árvore que guarda o ID da aplicação/tarefa +_ID_ROLE = Qt.ItemDataRole.UserRole + + +@dataclass(frozen=True) +class TaskMessage: + """Uma linha "$$$" de um log de processador.""" + + proc: str + app_id: int + task_id: int + message: str + + @classmethod + def parse(cls, line): + match = _MESSAGE_PATTERN.match(line.rstrip("\r\n")) + if match is None: + return None + return cls(match["proc"], int(match["app"]), int(match["task"]), match["message"]) + + @property + def proc_number(self): + """Endereço XY do processador (x << 8 | y).""" + x, y = (int(value) for value in self.proc.split("x")) + return (x << 8) | y + + +def read_name_relations(platform_path): + """ + Lê do platform.cfg as relações ID -> nome das tarefas e das aplicações. + Retorna (tarefas, aplicações). + """ + tasks, apps = {}, {} + section = None + + with open(platform_path, "r", encoding="utf-8") as platform_file: + for line in platform_file: + fields = line.split() + if not fields: + continue + + match fields[0]: + case "BEGIN_task_name_relation": + section = tasks + case "BEGIN_app_name_relation": + section = apps + case "END_task_name_relation" | "END_app_name_relation": + section = None + case name if section is not None and len(fields) >= 2: + try: + section[int(fields[1])] = name + except ValueError: + continue + + return tasks, apps + + +def read_task_messages(log_dir, app_id, task_id): + """ + Mensagens de uma tarefa em todos os logs do cenário, na ordem dos + arquivos (ordenados por nome) e das linhas. + """ + messages = [] + for file_name in sorted(os.listdir(log_dir)): + if "log" not in file_name: + continue + path = os.path.join(log_dir, file_name) + if not os.path.isfile(path): + continue + + with open(path, "r", encoding="utf-8", errors="replace") as log_file: + for line in log_file: + if not line.startswith(MESSAGE_MARK): + continue + message = TaskMessage.parse(line) + if message is not None and message.app_id == app_id and message.task_id == task_id: + messages.append(message) + + return messages + + +class DeloreamWindow(QMainWindow): + """Árvore Aplicações -> Tarefas e, ao ativar uma tarefa, as mensagens dela nos logs.""" + + def __init__(self, testcase_path, parent=None): + super().__init__(parent) + self.setWindowTitle("Deloream - Debug Log Reader for MPSoCs") + if os.path.isfile(ICON_PATH): + self.setWindowIcon(QIcon(ICON_PATH)) + self.resize(800, 700) + + self.tree = QTreeWidget() + self.tree.setHeaderHidden(True) + # Duplo clique ou Enter, como o duplo clique do original + self.tree.itemActivated.connect(self._on_item_activated) + + self.text = QTextBrowser() + self.text.setOpenLinks(False) + font = QFont("DejaVu Sans Mono") + font.setStyleHint(QFont.StyleHint.Monospace) + font.setPointSize(11) + self.text.setFont(font) + + splitter = QSplitter() + splitter.addWidget(self.tree) + splitter.addWidget(self.text) + splitter.setStretchFactor(1, 1) + splitter.setSizes([250, 550]) + self.setCentralWidget(splitter) + + self._build_menu() + self.load_testcase(testcase_path) + + def _build_menu(self): + options = self.menuBar().addMenu("Options") + + new_testcase = QAction("New testcase", self) + new_testcase.setShortcut(QKeySequence("Ctrl+N")) + new_testcase.triggered.connect(self._choose_testcase) + options.addAction(new_testcase) + + reload_action = QAction("Reload", self) + reload_action.setShortcut(QKeySequence("F5")) + reload_action.triggered.connect(lambda: self.load_testcase(self.testcase_path)) + options.addAction(reload_action) + + exit_action = QAction("Exit", self) + exit_action.setShortcut(QKeySequence("Ctrl+E")) + exit_action.triggered.connect(self.close) + options.addAction(exit_action) + + help_menu = self.menuBar().addMenu("Help") + about = QAction("About", self) + about.triggered.connect(lambda: QMessageBox.about( + self, "Deloream", + "Deloream - Debug Log Reader for MPSoCs\n\n" + "Double-click a task to read its debug messages ($$$) " + "from the processor logs of the testcase.")) + help_menu.addAction(about) + + # ========================================== + # CARREGAMENTO + # ========================================== + @property + def platform_path(self): + return os.path.join(self.testcase_path, "debug", "platform.cfg") + + @property + def log_dir(self): + return os.path.join(self.testcase_path, "log") + + def _choose_testcase(self): + path = QFileDialog.getExistingDirectory(self, "Open testcase (folder with debug/ and log/)", + self.testcase_path) + if path: + self.load_testcase(path) + + def load_testcase(self, testcase_path): + """Lê os nomes do platform.cfg e monta a árvore de aplicações.""" + self.testcase_path = testcase_path + self.setWindowTitle(f"Deloream - {testcase_path}") + self.tree.clear() + self.text.clear() + + try: + self.task_names, self.app_names = read_name_relations(self.platform_path) + except OSError: + self.task_names, self.app_names = {}, {} + self._show_error(f"platform.cfg not found in {html.escape(self.platform_path)}") + return + + root = QTreeWidgetItem(self.tree, ["Applications"]) + + # Aplicações do BEGIN_app_name_relation; tarefas sem aplicação conhecida + # também aparecem, com o ID da aplicação no lugar do nome + app_ids = sorted(set(self.app_names) | {task_id >> 8 for task_id in self.task_names}) + for app_id in app_ids: + app_name = self.app_names.get(app_id, "app") + app_item = QTreeWidgetItem(root, [f"{app_name}[{app_id}]"]) + app_item.setData(0, _ID_ROLE, app_id) + + for task_id in sorted(self.task_names): + if task_id >> 8 == app_id: + task_item = QTreeWidgetItem(app_item, [f"{self.task_names[task_id]}[{task_id}]"]) + task_item.setData(0, _ID_ROLE, task_id) + + self.tree.expandAll() + + # ========================================== + # MENSAGENS + # ========================================== + def _on_item_activated(self, item, _column): + # Só as folhas (tarefas) mostram mensagens + if item.parent() is None or item.parent().parent() is None: + return + self.show_task_messages(item.data(0, _ID_ROLE)) + + def show_task_messages(self, task_id): + app_id, local_task_id = task_id >> 8, task_id & 0xFF + + try: + messages = read_task_messages(self.log_dir, app_id, local_task_id) + except OSError: + messages = [] + + if not messages: + self._show_error("Log file not created yet or no message found!") + return + + name = html.escape(self.task_names.get(task_id, str(task_id))) + parts = [f"

{name}[{task_id}]

"] + current_proc = None + for message in messages: + # Cabeçalho a cada troca de processador (a tarefa pode ter migrado) + if message.proc != current_proc: + current_proc = message.proc + parts.append(f"

-- Processor: {html.escape(current_proc)}

") + parts.append(f"

{html.escape(message.message)}

") + + self.text.setHtml("".join(parts)) + + def _show_error(self, text): + self.text.setHtml(f"{text}") diff --git a/information/__init__.py b/information/__init__.py new file mode 100644 index 0000000..6f5585f --- /dev/null +++ b/information/__init__.py @@ -0,0 +1,25 @@ +""" +Modelo de dados do tráfego da NoC lido de traffic_router.txt. + +Refatoração em Python das classes do pacote "source" do GraphicalDebugger +original em Java (MPSoCInformation, RouterInformation, PacketInformation, +TaskInformation, PortInformation, RouterNeighbors e ReadTrafficData). +""" + +from information.packet_information import PacketInformation +from information.task_information import TaskInformation +from information.port_information import PortInformation +from information.router_neighbors import RouterNeighbors +from information.router_information import RouterInformation +from information.read_traffic_data import ReadTrafficData +from information.mpsoc_information import MPSoCInformation + +__all__ = [ + "PacketInformation", + "TaskInformation", + "PortInformation", + "RouterNeighbors", + "RouterInformation", + "ReadTrafficData", + "MPSoCInformation", +] diff --git a/information/mpsoc_information.py b/information/mpsoc_information.py new file mode 100644 index 0000000..acb8b0e --- /dev/null +++ b/information/mpsoc_information.py @@ -0,0 +1,82 @@ +from information.read_traffic_data import ReadTrafficData +from information.router_information import RouterInformation +from information.router_neighbors import RouterNeighbors + + +class MPSoCInformation: + """Estado de todos os roteadores do MPSoC, alimentado pelos pacotes de traffic_router.txt.""" + + def __init__(self, mpsoc_config): + self.mpsoc_config = mpsoc_config + self.initialize_pe_information() + self.neighbors = RouterNeighbors.from_config(mpsoc_config) + self.read_traffic = ReadTrafficData(mpsoc_config, self.neighbors) + + def initialize_pe_information(self): + self.pe_information = { + address: RouterInformation(self.mpsoc_config, address) + for address in range(self.mpsoc_config.get_pe_number()) + } + + def close(self): + self.read_traffic.close() + + def get_packet_counter_by_time(self, time): + """Retorna o número de pacotes até chegar no tempo desejado.""" + counter = self.read_traffic.get_packet_counter_by_time(time) + + if counter != -1: + self.read_traffic.reset_packet_counter() + + return counter + + def get_router_information(self, router_address): + return self.pe_information.get(router_address) + + def get_router_information_xy(self, x, y): + return self.get_router_information(self.neighbors.xy_to_ham_addr((x << 8) | y)) + + # ========================================== + # TOTAIS DA NOC + # ========================================== + def get_total_noc_volume(self): + return sum(router.get_router_total_volume_in_flits() for router in self.pe_information.values()) + + def get_total_noc_bandwidth(self): + return sum(router.get_router_total_bandwidth_in_cycles() for router in self.pe_information.values()) + + def get_total_noc_service_volume(self, services): + return sum(router.get_router_total_services_volume_in_flits(services) + for router in self.pe_information.values()) + + def get_total_noc_service_bandwidth(self, services): + return sum(router.get_router_total_services_bandwidth_in_cycles(services) + for router in self.pe_information.values()) + + def get_next_packet(self, packet_filter=None, limit_time=0): + """ + Lê o próximo pacote e o registra no roteador em que foi capturado. + + packet_filter é uma função (packet -> bool); pacotes rejeitados são + pulados enquanto estiverem antes de limit_time (0 = sem limite). + Retorna None quando não há mais pacotes. + """ + while True: + packet = self.read_traffic.get_next_packet() + + if packet is None: + return None + + # Serviços desconhecidos são repassados sem atualizar os roteadores + if packet.service not in self.mpsoc_config.services_hash: + return packet + + if packet_filter is not None and not packet_filter(packet): + if limit_time == 0 or limit_time > packet.time: + continue + + router = self.pe_information.get(packet.router_address) + if router is not None: + router.add_packet(packet) + + return packet diff --git a/information/packet_information.py b/information/packet_information.py new file mode 100644 index 0000000..ff5ae6c --- /dev/null +++ b/information/packet_information.py @@ -0,0 +1,42 @@ +from dataclasses import dataclass, field + +from util.MPSoCConfig import MPSoCConfig + + +@dataclass +class PacketInformation: + """Um pacote registrado em traffic_router.txt ao passar por um roteador.""" + + router_address: int + time: int + service: int + size: int + bandwidth_cycles: int + input_port: int + target_router: int + task_source: int = -1 + task_target: int = -1 + channel: int = field(init=False) + + def __post_init__(self): + # O canal é definido pela porta de entrada (portas pares = HIGH) + self.channel = MPSoCConfig.get_channel(self.input_port) + + def get_input_port_string(self): + return MPSoCConfig.get_port_string(self.input_port) + + def get_channel_string(self): + return "HIGH" if self.channel == MPSoCConfig.HIGH else "LOW" + + def print_packet(self): + print("*********PACKET************") + print(f"router_address: {self.router_address}") + print(f"time: {self.time}") + print(f"service: {self.service}") + print(f"size: {self.size}") + print(f"bandwidth cycles: {self.bandwidth_cycles}") + print(f"channel: {self.get_channel_string()}") + print(f"input_port: {self.get_input_port_string()}") + print(f"target_router: {self.target_router}") + print(f"task_source: {self.task_source}") + print(f"task_target: {self.task_target}") diff --git a/information/port_information.py b/information/port_information.py new file mode 100644 index 0000000..a80465f --- /dev/null +++ b/information/port_information.py @@ -0,0 +1,40 @@ +from collections import defaultdict + + +class PortInformation: + """Volume e largura de banda acumulados em uma porta de um roteador.""" + + def __init__(self, mpsoc_config, channel): + self.channel = channel + self.total_volume = 0 + self.total_bandwidth = 0 + self.throughput = 0 + self.bandwidth_throughput = 0 + + # Serviços desconhecidos começam em zero em vez de gerar erro + self.service_volume = defaultdict(int, dict.fromkeys(mpsoc_config.service_reference, 0)) + self.service_bandwidth = defaultdict(int, dict.fromkeys(mpsoc_config.service_reference, 0)) + + def add_new_information(self, packet): + volume = packet.size + 1 # +1 por causa do header + + self.total_volume += volume + self.throughput += volume + + self.total_bandwidth += packet.bandwidth_cycles + self.bandwidth_throughput += packet.bandwidth_cycles + + self.service_volume[packet.service] += volume + self.service_bandwidth[packet.service] += packet.bandwidth_cycles + + def get_service_volume_in_flits(self, service): + return self.service_volume.get(service, 0) + + def get_service_bandwidth_in_cycles(self, service): + return self.service_bandwidth.get(service, 0) + + def reset_port_throughput(self): + self.throughput = 0 + + def reset_port_bandwidth_throughput(self): + self.bandwidth_throughput = 0 diff --git a/information/read_traffic_data.py b/information/read_traffic_data.py new file mode 100644 index 0000000..04fdf66 --- /dev/null +++ b/information/read_traffic_data.py @@ -0,0 +1,86 @@ +import os +from bisect import bisect_right + +from util.MPSoCConfig import MPSoCConfig +from information.packet_information import PacketInformation + + +class ReadTrafficData: + """ + Lê os pacotes de traffic_router.txt sob demanda. + + Os pacotes já lidos ficam em all_packets, assim é possível voltar no tempo + (reset_packet_counter) sem precisar reler o arquivo. + """ + + def __init__(self, mpsoc_config, neighbors): + self.mpsoc_config = mpsoc_config + self.neighbors = neighbors + self.packet_read_control = 0 + self.all_packets = [] + + traffic_path = os.path.join(mpsoc_config.debug_file_path, "traffic_router.txt") + # Gera FileNotFoundError caso o arquivo não exista + self.traffic = open(traffic_path, "r", encoding="utf-8") + + def close(self): + self.traffic.close() + + def reset_packet_counter(self): + self.packet_read_control = 0 + + def get_packet_counter_by_time(self, time): + """Retorna quantos pacotes já lidos ocorreram até o tempo informado (-1 se fora do intervalo).""" + if not self.all_packets or time < 0 or self.all_packets[-1].time < time: + return -1 + + return bisect_right(self.all_packets, time, key=lambda packet: packet.time) + + def get_next_packet(self): + # Ainda não lido: busca a próxima linha válida do arquivo + if self.packet_read_control == len(self.all_packets): + for line in self.traffic: + packet = self._parse_packet(line) + if packet is not None: + self.all_packets.append(packet) + self.packet_read_control += 1 + return packet + return None + + packet = self.all_packets[self.packet_read_control] + self.packet_read_control += 1 + return packet + + def _parse_packet(self, line): + fields = line.strip().split("\t") + if fields == [""]: + return None + + try: + time = int(fields[0]) + router_address = self._extract_router_address(fields[1]) + service = int(fields[2]) + size = int(fields[3]) + bandwidth_cycles = int(fields[4]) + + if self.mpsoc_config.channel_number == 1: + # Suporte a canal físico duplicado + input_port = int(fields[5]) * 2 + 1 + else: + input_port = int(fields[5]) + + target_router = self._extract_router_address(fields[6]) + task_source = int(fields[7]) if len(fields) > 7 else -1 + task_target = int(fields[8]) if len(fields) > 8 else -1 + except (ValueError, IndexError): + print(f"WARNING: Wrong packet format in traffic_router.txt: {line.strip()}") + return None + + return PacketInformation(router_address, time, service, size, bandwidth_cycles, + input_port, target_router, task_source, task_target) + + def _extract_router_address(self, value): + address = int(value) + if self.mpsoc_config.router_addressing == MPSoCConfig.XY: + return self.neighbors.xy_to_ham_addr(address) + return address diff --git a/information/router_information.py b/information/router_information.py new file mode 100644 index 0000000..a8877ee --- /dev/null +++ b/information/router_information.py @@ -0,0 +1,101 @@ +from util.MPSoCConfig import MPSoCConfig +from information.port_information import PortInformation +from information.task_information import TaskInformation + + +class RouterInformation: + """Pacotes, portas e eventos de tarefa acumulados em um roteador (PE).""" + + def __init__(self, mpsoc_config, router_address): + self.router_address = router_address + self.mpsoc_config = mpsoc_config + self.task_counter = 0 + self.packets = [] + self.tasks = [] + self.port_informations = [ + PortInformation(mpsoc_config, MPSoCConfig.get_channel(port)) + for port in range(MPSoCConfig.NPORT) + ] + + def add_packet(self, packet): + self.packets.append(packet) + self.port_informations[packet.input_port].add_new_information(packet) + self._update_task_information(packet) + + def _update_task_information(self, packet): + config = self.mpsoc_config + + # Pacote está entrando no PE + if packet.target_router == self.router_address: + if packet.service in config.task_allocation_services: + self.task_counter += 1 + self.tasks.append(TaskInformation(packet.task_source, "ALLOCATED", packet.time)) + elif packet.service == config.get_service_value("MESSAGE_DELIVERY"): + self.tasks.append(TaskInformation(packet.task_target, "MESSAGE_DELIVERY", + packet.time, packet.task_source)) + + # Pacote está saindo do PE + elif packet.input_port in (MPSoCConfig.LOCAL0, MPSoCConfig.LOCAL1): + if packet.service in config.task_terminated_services: + self.tasks.append(TaskInformation(packet.task_source, "TERMINATED", packet.time)) + elif packet.service == config.get_service_value("MESSAGE_REQUEST"): + self.tasks.append(TaskInformation(packet.task_target, "MESSAGE_REQUEST", + packet.time, packet.task_source)) + + def get_tasks_information(self): + return self.tasks + + def get_task_number(self): + return self.task_counter + + # ========================================== + # TOTAIS DO ROTEADOR (SOMA DE TODAS AS PORTAS) + # ========================================== + def get_router_total_throughput_in_flits(self): + return sum(port.throughput for port in self.port_informations) + + def get_router_total_bandwidth_throughput_in_cycles(self): + return sum(port.bandwidth_throughput for port in self.port_informations) + + def get_router_total_volume_in_flits(self): + return sum(port.total_volume for port in self.port_informations) + + def get_router_total_bandwidth_in_cycles(self): + return sum(port.total_bandwidth for port in self.port_informations) + + def get_router_total_services_volume_in_flits(self, services): + return sum(self.get_port_volume_in_flits(port, services) for port in range(MPSoCConfig.NPORT)) + + def get_router_total_services_bandwidth_in_cycles(self, services): + return sum(self.get_port_bandwidth_in_cycles(port, services) for port in range(MPSoCConfig.NPORT)) + + # ========================================== + # INFORMAÇÕES POR PORTA + # ========================================== + def get_port_total_volume_in_flits(self, port): + return self.port_informations[port].total_volume + + def get_port_total_bandwidth_in_cycles(self, port): + return self.port_informations[port].total_bandwidth + + def get_port_volume_in_flits(self, port, services): + port_info = self.port_informations[port] + return sum(port_info.get_service_volume_in_flits(service) for service in services) + + def get_port_bandwidth_in_cycles(self, port, services): + port_info = self.port_informations[port] + return sum(port_info.get_service_bandwidth_in_cycles(service) for service in services) + + def get_port_throughput_in_flits(self, port): + return self.port_informations[port].throughput + + def get_port_bandwidth_throughput_in_cycles(self, port): + return self.port_informations[port].bandwidth_throughput + + def reset_throughput(self): + for port in self.port_informations: + port.reset_port_throughput() + + def reset_bandwidth_throughput(self): + for port in self.port_informations: + port.reset_port_bandwidth_throughput() diff --git a/information/router_neighbors.py b/information/router_neighbors.py new file mode 100644 index 0000000..fdd67e7 --- /dev/null +++ b/information/router_neighbors.py @@ -0,0 +1,59 @@ +class RouterNeighbors: + """ + Calcula os vizinhos de um roteador a partir do seu endereço hamiltoniano. + Os métodos retornam -1 quando o vizinho não existe (borda da malha). + """ + + def __init__(self, x_dimension, y_dimension): + self.x_dimension = x_dimension + self.y_dimension = y_dimension + + @classmethod + def from_config(cls, mpsoc_config): + return cls(mpsoc_config.mpsoc_x, mpsoc_config.mpsoc_y) + + def xy_to_ham_addr(self, xy_addr): + xy_addr &= 0xFFFF # limpa o header + y = xy_addr & 0xFF + x = xy_addr >> 8 + + if y % 2 == 1: + return (y * self.x_dimension) + (self.x_dimension - x) - 1 + return (y * self.x_dimension) + x + + def ham_to_xy_addr(self, ham_addr): + ham_addr &= 0xFF # limpa o header + y, addr = divmod(ham_addr, self.x_dimension) + + x = self.x_dimension - addr - 1 if y % 2 == 1 else addr + return (x << 8) | y + + def xy_address_to_xy_label(self, ham_addr): + xy_addr = self.ham_to_xy_addr(ham_addr) + return f"{xy_addr >> 8}{xy_addr & 0xFF}" + + def get_x_coordinate(self, ham_addr): + return self.ham_to_xy_addr(ham_addr) >> 8 + + def get_y_coordinate(self, ham_addr): + return self.ham_to_xy_addr(ham_addr) & 0xFF + + def _neighbor(self, ham_addr, dx, dy): + x = self.get_x_coordinate(ham_addr) + dx + y = self.get_y_coordinate(ham_addr) + dy + + if 0 <= x < self.x_dimension and 0 <= y < self.y_dimension: + return self.xy_to_ham_addr((x << 8) | y) + return -1 + + def get_vizinho_cima(self, ham_addr): + return self._neighbor(ham_addr, 0, 1) + + def get_vizinho_baixo(self, ham_addr): + return self._neighbor(ham_addr, 0, -1) + + def get_vizinho_esquerda(self, ham_addr): + return self._neighbor(ham_addr, -1, 0) + + def get_vizinho_direita(self, ham_addr): + return self._neighbor(ham_addr, 1, 0) diff --git a/information/task_information.py b/information/task_information.py new file mode 100644 index 0000000..9c88457 --- /dev/null +++ b/information/task_information.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass + + +@dataclass +class TaskInformation: + """Um evento de tarefa (alocação, término, mensagem) ocorrido em um PE.""" + + id: int + service: str + time: int + remote_task_id: int = -1 + + @property + def task_id(self): + # Os 8 bits menos significativos identificam a tarefa dentro da aplicação + return self.id & 0xFF + + @property + def app_id(self): + return self.id >> 8 diff --git a/overview_windows.py b/overview_windows.py new file mode 100644 index 0000000..f03a7cd --- /dev/null +++ b/overview_windows.py @@ -0,0 +1,439 @@ +""" +Janelas "Communication Overview", "Task Mapping Overview" e "Message Log". + +Refatoração de CommunicationOverview.java, TaskMappingFrame.java e da +janela de Message Log (MainFrame.showMessageList) do GraphicalDebugger +original. As duas janelas leem o estado dos roteadores +(MPSoCInformation) e se atualizam sozinhas enquanto a simulação da janela +principal avança. +""" + +import os +import random + +from PySide6.QtCore import Qt, QTimer +from PySide6.QtGui import QColor, QPixmap +from PySide6.QtWidgets import (QHeaderView, QListWidget, QMessageBox, QTableWidgetItem, QVBoxLayout, + QWidget) + +from commsUi import Ui_Form as Ui_CommunicationOverview +from pe_matrix import PEMatrixWidget +from slave_matrix import SlaveMatrixWidget +from taskmap import Ui_TaskMap +from util.MPSoCConfig import MPSoCConfig + +IMAGES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "images") + +# Intervalo mínimo entre duas atualizações da janela durante a simulação +REFRESH_INTERVAL_MS = 500 + +_PE_TYPE_NAMES = { + MPSoCConfig.GLOBAL_MASTER: "Global M", + MPSoCConfig.CLUSTER_MASTER: "Cluster M", + MPSoCConfig.SLAVE: "Slave", +} + + +def _router_label(mpsoc_config, address): + if mpsoc_config.router_addressing == MPSoCConfig.HAMILTONIAN: + return str(address) + return mpsoc_config.ham_address_to_xy_label(address) + + +class _LiveOverview(QWidget): + """ + Base das janelas que acompanham a simulação. + + Em vez de redesenhar a cada pacote (o que travaria o modo rápido), a + janela só marca que os dados mudaram; um QTimer redesenha no máximo a + cada REFRESH_INTERVAL_MS, e apenas se a janela estiver visível. + """ + + def __init__(self, mpsoc_config, mpsoc_information, simulation=None): + super().__init__() + self.mpsoc_config = mpsoc_config + self.mpsoc_information = mpsoc_information + self.simulation = None + self.matrix = None + self.live_update = True + self._dirty = True + + self._refresh_timer = QTimer(self) + self._refresh_timer.timeout.connect(self._refresh_if_dirty) + self._refresh_timer.start(REFRESH_INTERVAL_MS) + + self._connect_simulation(simulation) + + def _connect_simulation(self, simulation): + if self.simulation is not None: + try: + self.simulation.time_changed.disconnect(self.mark_dirty) + except (RuntimeError, TypeError): + pass # controlador antigo já destruído + self.simulation = simulation + if simulation is not None: + simulation.time_changed.connect(self.mark_dirty) + + def set_information(self, mpsoc_config, mpsoc_information, simulation=None): + """Troca a fonte de dados (novo debug ou Reset Simulation).""" + self.mpsoc_config = mpsoc_config + self.mpsoc_information = mpsoc_information + self._connect_simulation(simulation) + self.rebuild() + + def mark_dirty(self, *_): + self._dirty = True + + def _refresh_if_dirty(self): + if self._dirty and self.live_update and self.isVisible(): + self.refresh() + + def showEvent(self, event): + super().showEvent(event) + self.refresh() + + def closeEvent(self, event): + self._refresh_timer.stop() + self._connect_simulation(None) + super().closeEvent(event) + + def _set_matrix(self, matrix): + """Coloca a grade dentro do scrollArea gerado pelo .ui, trocando a anterior.""" + layout = self.scrollAreaWidgetContents.layout() + if layout is None: + layout = QVBoxLayout(self.scrollAreaWidgetContents) + layout.setContentsMargins(0, 0, 0, 0) + while layout.count(): + old = layout.takeAt(0).widget() + if old is not None: + old.deleteLater() + layout.addWidget(matrix) + + def rebuild(self): + raise NotImplementedError + + def refresh(self): + raise NotImplementedError + + +class CommunicationOverviewWindow(_LiveOverview, Ui_CommunicationOverview): + """ + Mapa de calor do tráfego: cada célula mostra a porcentagem do volume + (flits) ou da banda (ciclos) que passou pelo roteador. + + Filtros (iguais ao Java): + - All Services: todo o tráfego; senão, só o serviço escolhido, com a + porcentagem calculada sobre + Global -> tráfego desse serviço em toda a NoC + All traffic -> todo o tráfego da NoC + Router -> todo o tráfego do próprio roteador + - Volume / Bandwidth: flits ou ciclos de enlace + """ + + def __init__(self, mpsoc_config, mpsoc_information, simulation=None): + super().__init__(mpsoc_config, mpsoc_information, simulation) + self.setupUi(self) + self.setWindowTitle("Communication Overview") + + # O .ui usa um caminho relativo que só funciona rodando da pasta do projeto + self.label.setPixmap(QPixmap(os.path.join(IMAGES_DIR, "color_spectrum.png"))) + self.label_3.setText("high") + self.label_3.setAlignment(Qt.AlignmentFlag.AlignRight) + + # Estatísticas: uma linha, colunas Bigger / Average / Small + self.tableWidget.setRowCount(1) + self.tableWidget.verticalHeader().setVisible(False) + self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + # Largura do original (379 px) para caber "R. 2x2: 19.472%" sem cortar + self.groupBox_3.setMinimumWidth(380) + + # Estado inicial igual ao Java: todos os serviços, "Global", volume + self.checkBox.setChecked(True) + self.radioButton_2.setChecked(True) + self.radioButton_4.setChecked(True) + + self.checkBox.toggled.connect(self._on_filter_changed) + self.comboBox.currentIndexChanged.connect(self._on_filter_changed) + for radio in (self.radioButton, self.radioButton_2, self.radioButton_3, + self.radioButton_4, self.radioButton_5): + radio.toggled.connect(self._on_radio_toggled) + + self.rebuild() + + def rebuild(self): + """Recria a grade e a lista de serviços para a configuração atual.""" + config = self.mpsoc_config + + self.comboBox.blockSignals(True) + self.comboBox.clear() + for service in config.service_reference: + self.comboBox.addItem(config.get_string_service_name(service), service) + self.comboBox.blockSignals(False) + + self.matrix = SlaveMatrixWidget(config.mpsoc_x, config.mpsoc_y) + self._set_matrix(self.matrix) + self._update_filter_state() + self.refresh() + + def _on_radio_toggled(self, checked): + # Cada troca dispara dois toggled (um desmarca, outro marca); basta um + if checked: + self.refresh() + + def _on_filter_changed(self, *_): + self._update_filter_state() + self.refresh() + + def _update_filter_state(self): + filter_by_service = not self.checkBox.isChecked() + for widget in (self.comboBox, self.radioButton, self.radioButton_2, self.radioButton_3): + widget.setEnabled(filter_by_service) + if not filter_by_service: + self.radioButton_2.setChecked(True) + + def selected_services(self): + """Lista de serviços filtrados; vazia = todos.""" + if self.checkBox.isChecked() or self.comboBox.currentIndex() < 0: + return [] + return [self.comboBox.currentData()] + + def refresh(self): + self._dirty = False + if self.matrix is None: + return + + config = self.mpsoc_config + info = self.mpsoc_information + services = self.selected_services() + use_volume = self.radioButton_4.isChecked() + unit = "flits" if use_volume else "cycles" + + def router_total(router): + if use_volume: + return router.get_router_total_volume_in_flits() + return router.get_router_total_bandwidth_in_cycles() + + def router_value(router): + if not services: + return router_total(router) + if use_volume: + return router.get_router_total_services_volume_in_flits(services) + return router.get_router_total_services_bandwidth_in_cycles(services) + + addresses = range(config.get_pe_number()) + values = {address: router_value(info.get_router_information(address)) for address in addresses} + + # Base da porcentagem + if self.radioButton.isChecked() and services: # All traffic + noc_total = info.get_total_noc_volume() if use_volume else info.get_total_noc_bandwidth() + else: # Global + noc_total = sum(values.values()) + + percents = {} + for address in addresses: + if self.radioButton_3.isChecked() and services: # Router + total = router_total(info.get_router_information(address)) + else: + total = noc_total + percents[address] = values[address] * 100.0 / total if total else 0.0 + + for (x, y), cell in self.matrix.slaves.items(): + address = config.xy_to_ham_addr((x << 8) | y) + percent = percents[address] + + # Mesma escala do Java: azul (0%) até vermelho (100%) + hue = 0.65 - percent * 0.65 / 100.0 + color = QColor.fromHsvF(max(0.0, hue), 1.0, 0.8) + + title = f"{_PE_TYPE_NAMES[config.get_pe_type(address)]} {_router_label(config, address)}" + cell.set_values(title, f"{percent:.3f}%", unit, color) + + self._update_statistics(percents) + + def _update_statistics(self, percents): + if not percents: + return + config = self.mpsoc_config + + # Em empate, fica o primeiro roteador na ordem de endereço + bigger = max(percents, key=lambda address: percents[address]) + smaller = min(percents, key=lambda address: percents[address]) + average = sum(percents.values()) / len(percents) + + values = ( + f"R. {_router_label(config, bigger)}: {percents[bigger]:.3f}%", + f"{average:.3f}%", + f"R. {_router_label(config, smaller)}: {percents[smaller]:.3f}%", + ) + for column, value in enumerate(values): + self.tableWidget.setItem(0, column, QTableWidgetItem(value)) + + +class TaskMappingWindow(_LiveOverview, Ui_TaskMap): + """ + Mostra em cada PE as tarefas alocadas (RUN) e terminadas (TER) até o + tempo atual da simulação, com uma cor por aplicação. + """ + + # Semente fixa: cada aplicação mantém a mesma cor entre execuções + COLOR_SEED = 20 + + def __init__(self, mpsoc_config, mpsoc_information, simulation=None): + super().__init__(mpsoc_config, mpsoc_information, simulation) + self.setupUi(self) + + # O .ui repete "All tasks status" no terceiro botão + self.radioButton_3.setText("Only Terminated") + + # "Only Running" é o filtro inicial, como no Java + self.radioButton_2.setChecked(True) + for radio in (self.radioButton, self.radioButton_2, self.radioButton_3): + radio.toggled.connect(self._on_radio_toggled) + self.checkBox.toggled.connect(self.refresh) + + # Botão alterna entre atualização automática e manual + self.pushButton.setCheckable(True) + self.pushButton.setChecked(True) + self.pushButton.toggled.connect(self._on_update_toggled) + self._on_update_toggled(True) + + self._warned_missing_names = False + self.rebuild() + + def rebuild(self): + config = self.mpsoc_config + self.setWindowTitle(f"Task Mapping Overview: {config.get_testcase_path() or config.debug_file_path}") + + self.app_colors = {} + self.color_generator = random.Random(self.COLOR_SEED) + + self.matrix = PEMatrixWidget(config.mpsoc_x, config.mpsoc_y) + self._set_matrix(self.matrix) + self.refresh() + + def _on_radio_toggled(self, checked): + if checked: + self.refresh() + + def _on_update_toggled(self, checked): + self.live_update = checked + self.pushButton.setText("Updating" if checked else "Update") + self.refresh() + + def showEvent(self, event): + super().showEvent(event) + if not self.mpsoc_config.task_name_hash and not self._warned_missing_names: + self._warned_missing_names = True + QMessageBox.warning(self, "Warning", "Task name representation not informed\n" + "The task ID will be represented by numbers") + + def _app_color(self, app_id): + if app_id not in self.app_colors: + self.app_colors[app_id] = QColor.fromRgb(self.color_generator.randrange(0xFFFFFF)) + return self.app_colors[app_id] + + def refresh(self): + self._dirty = False + if self.matrix is None: + return + + config = self.mpsoc_config + task_names = config.task_name_hash + only_running = self.radioButton_2.isChecked() + only_terminated = self.radioButton_3.isChecked() + without_id = self.checkBox.isChecked() + + for (x, y), cell in self.matrix.pes.items(): + address = config.xy_to_ham_addr((x << 8) | y) + router = self.mpsoc_information.get_router_information(address) + + # Último estado de cada tarefa neste PE + status = {} + for task in router.get_tasks_information(): + if task.service == "ALLOCATED": + status[task.id] = "RUN" + elif task.service == "TERMINATED": + status[task.id] = "TER" + + tasks = [] + for task_id in sorted(status): + if only_running and status[task_id] == "TER": + continue + if only_terminated and status[task_id] == "RUN": + continue + + name = task_names.get(task_id, f"{task_id >> 8} {task_id & 0xFF}") + text = f"{name} {status[task_id]}" if without_id else f"{name} {task_id} {status[task_id]}" + tasks.append((text, self._app_color(task_id >> 8))) + + cell.set_tasks(f"PE {_router_label(config, address)}", tasks) + + +class MessageLogWindow(_LiveOverview): + """ + Lista as mensagens trocadas entre os PEs, na ordem em que aparecem no + traffic_router.txt: + + ---->>> origem send message to destino service SERVIÇO + (pacote entrando na NoC pela porta local do PE de origem) + <<<---- destino receive message with service SERVIÇO + (pacote chegando ao roteador de destino) + + Mostra todos os pacotes já lidos e acrescenta os novos conforme a + simulação avança. + """ + + def __init__(self, mpsoc_config, mpsoc_information, simulation=None): + super().__init__(mpsoc_config, mpsoc_information, simulation) + self.setWindowTitle("Message Log") + self.resize(500, 600) + + self.list_widget = QListWidget(self) + self.list_widget.setUniformItemSizes(True) # listas longas continuam rápidas + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.list_widget) + + self._packets_listed = 0 + self.rebuild() + + def rebuild(self): + self.list_widget.clear() + self._packets_listed = 0 + self.refresh() + + def _message(self, packet): + config = self.mpsoc_config + service = config.get_string_service_name(packet.service) + router = _router_label(config, packet.router_address) + message = None + + if packet.input_port in (MPSoCConfig.LOCAL0, MPSoCConfig.LOCAL1): + target = _router_label(config, packet.target_router) + message = f"---->>> {router} send message to {target} service {service}" + + if packet.router_address == packet.target_router: + message = f"<<<---- {router} receive message with service {service}" + + return message + + def refresh(self): + self._dirty = False + + # all_packets só cresce (voltar no tempo reaproveita os pacotes já lidos), + # então basta acrescentar os pacotes novos desde a última atualização + packets = self.mpsoc_information.read_traffic.all_packets + new_packets = packets[self._packets_listed:] + if not new_packets: + return + self._packets_listed = len(packets) + + scrollbar = self.list_widget.verticalScrollBar() + at_bottom = scrollbar.value() == scrollbar.maximum() + + messages = [message for message in map(self._message, new_packets) if message is not None] + self.list_widget.addItems(messages) + + # Acompanha o fim da lista, a menos que o usuário tenha rolado para cima + if at_bottom: + self.list_widget.scrollToBottom() diff --git a/simulation.py b/simulation.py new file mode 100644 index 0000000..b0a4d1c --- /dev/null +++ b/simulation.py @@ -0,0 +1,282 @@ +""" +Controle da simulação dos roteadores (play / próximo pacote / stop / voltar no tempo). + +Refatoração da parte de simulação do MainFrame.java e do CheckpointController.java +do GraphicalDebugger original. A thread com sleep() do Java foi trocada por um +QTimer, que roda no laço de eventos do Qt e pode atualizar a interface com segurança. +""" + +from PySide6.QtCore import QObject, QTimer, Signal + +from util.MPSoCConfig import MPSoCConfig + +# Velocidade (slider 0-100) acima da qual os roteadores não são redesenhados, +# para avançar o mais rápido possível +MAX_REPAINT_SPEED = 98 + +# Pacotes processados por passo do timer quando está sem redesenhar +FAST_BATCH_SIZE = 200 + +# No "voltar no tempo", só os últimos pacotes são desenhados +GO_REPAINT_LAST_PACKETS = 50 + + +class CheckpointController: + """ + Divide a simulação em janelas de tempo. Ao fim de cada janela a taxa de uso + dos enlaces é recalculada (checkpoint_reached). + """ + + def __init__(self, clock_period_ns, checkpoint_reached, window_size_ms=0.5): + self.clock_period_ns = clock_period_ns + self.checkpoint_reached = checkpoint_reached + self.window_size_ms = window_size_ms + self.reset() + + def reset(self): + self.window_init_ms = 0.0 + self.current_time_ms = 0.0 + + def set_time(self, time_in_ticks): + self.current_time_ms = ticks_to_ms(time_in_ticks, self.clock_period_ns) + + if self.current_time_ms - self.window_init_ms > self.window_size_ms: + self.window_init_ms = self.current_time_ms + self.checkpoint_reached() + + def get_window_size_in_ns(self): + return self.window_size_ms * 1_000_000 + + +def ticks_to_ms(ticks, clock_period_ns): + return ticks * clock_period_ns / 1_000_000 + + +# Porta de entrada -> (seta no roteador atual, seta no vizinho de onde o pacote veio) +_INPUT_ARROWS = { + MPSoCConfig.EAST0: (MPSoCConfig.EAST_IN_HIGH, MPSoCConfig.WEST_OUT_HIGH), + MPSoCConfig.EAST1: (MPSoCConfig.EAST_IN_LOW, MPSoCConfig.WEST_OUT_LOW), + MPSoCConfig.NORTH0: (MPSoCConfig.NORTH_IN_HIGH, MPSoCConfig.SOUTH_OUT_HIGH), + MPSoCConfig.NORTH1: (MPSoCConfig.NORTH_IN_LOW, MPSoCConfig.SOUTH_OUT_LOW), + MPSoCConfig.WEST0: (MPSoCConfig.WEST_IN_HIGH, MPSoCConfig.EAST_OUT_HIGH), + MPSoCConfig.WEST1: (MPSoCConfig.WEST_IN_LOW, MPSoCConfig.EAST_OUT_LOW), + MPSoCConfig.SOUTH0: (MPSoCConfig.SOUTH_IN_HIGH, MPSoCConfig.NORTH_OUT_HIGH), + MPSoCConfig.SOUTH1: (MPSoCConfig.SOUTH_IN_LOW, MPSoCConfig.NORTH_OUT_LOW), +} + + +class SimulationController(QObject): + """Reproduz os pacotes de traffic_router.txt sobre a matriz de roteadores.""" + + # Tempo (em ticks) do último pacote processado + time_changed = Signal(int) + # Último pacote processado (PacketInformation) + packet_changed = Signal(object) + # Play/stop mudou (True = rodando) + running_changed = Signal(bool) + # Fim do traffic_router.txt + finished = Signal() + # Pacote com serviço que não existe no services.cfg + unknown_service = Signal(object) + + def __init__(self, mpsoc_config, mpsoc_information, router_matrix, parent=None): + super().__init__(parent) + self.mpsoc_config = mpsoc_config + self.mpsoc_information = mpsoc_information + self.router_matrix = router_matrix + + # Função (packet -> bool) usada pelo filtro de serviços/PEs; None = todos + self.packet_filter = None + self.speed = MAX_REPAINT_SPEED + self.running = False + + # Roteadores de destino dos pacotes que saíram de um PE e ainda não + # chegaram; enquanto houver algum, as setas pintadas não são apagadas + self.unfinished_packets = [] + + self.checkpoint = CheckpointController(mpsoc_config.clock_period_in_ns, self.checkpoint_reached) + + self.timer = QTimer(self) + self.timer.timeout.connect(self._play_step) + + # ========================================== + # CONTROLES + # ========================================== + def set_speed(self, speed): + """Velocidade de 0 (parado) a 100 (máxima, sem redesenhar as setas).""" + self.speed = speed + if self.running: + self.timer.setInterval(self._interval()) + + def _interval(self): + return max(0, 400 - self.speed * 4) + + def play(self): + if self.running: + return + self.running = True + self.timer.start(self._interval()) + self.running_changed.emit(True) + + def stop(self): + if not self.running: + return + self.running = False + self.timer.stop() + self.running_changed.emit(False) + + def step(self): + """Processa um único pacote, sempre redesenhando.""" + return self.next_packet(repaint=True) + + def _play_step(self): + if self.speed == 0: + return + + if self.speed > MAX_REPAINT_SPEED: + # Modo rápido: vários pacotes por passo e sem desenhar as setas + self.unfinished_packets.clear() + for _ in range(FAST_BATCH_SIZE): + if self.next_packet(repaint=False) == -1 or not self.running: + return + else: + self.next_packet(repaint=True) + + def go_to_time(self, desired_time): + """ + Volta (ou avança) a simulação até o tempo informado, em ticks. + Só é possível ir até o último pacote já lido. Gera ValueError se o + tempo for inválido. + """ + packet_counter = self.mpsoc_information.get_packet_counter_by_time(desired_time) + if packet_counter == -1: + raise ValueError(f"Invalid time: {desired_time}") + + self.stop() + self.mpsoc_information.initialize_pe_information() + self.checkpoint.reset() + self.reset_graphical_path() + + time = 0 + while packet_counter > 0: + time = self.next_packet(packet_counter <= GO_REPAINT_LAST_PACKETS, desired_time) + if time == -1 or time > desired_time: + break + packet_counter -= 1 + + self.time_changed.emit(max(time, 0)) + + def reset_graphical_path(self): + self.unfinished_packets.clear() + self.router_matrix.reset_routers() + + # ========================================== + # PROCESSAMENTO DOS PACOTES + # ========================================== + def next_packet(self, repaint=True, limit_time=0): + """Processa o próximo pacote. Retorna o tempo dele ou -1 no fim do arquivo.""" + packet = self.mpsoc_information.get_next_packet(self.packet_filter, limit_time) + + if packet is None: + self.router_matrix.reset_routers() + self.stop() + self.finished.emit() + return -1 + + if packet.service not in self.mpsoc_config.services_hash: + self.stop() + self.unknown_service.emit(packet) + + self.checkpoint.set_time(packet.time) + + if repaint: + self.repaint_router(packet) + + self.packet_changed.emit(packet) + self.time_changed.emit(packet.time) + return packet.time + + def repaint_router(self, packet): + """Pinta o caminho do pacote: seta de entrada no roteador e de saída no vizinho.""" + router = self.router_matrix.get_router_by_address(packet.router_address) + if router is None: + return + + port = packet.input_port + neighbors = self.mpsoc_information.neighbors + address = packet.router_address + + if port in _INPUT_ARROWS: + local_arrow, neighbor_arrow = _INPUT_ARROWS[port] + + if port in (MPSoCConfig.NORTH0, MPSoCConfig.NORTH1): + neighbor_address = neighbors.get_vizinho_cima(address) + elif port in (MPSoCConfig.SOUTH0, MPSoCConfig.SOUTH1): + neighbor_address = neighbors.get_vizinho_baixo(address) + elif port in (MPSoCConfig.EAST0, MPSoCConfig.EAST1): + neighbor_address = neighbors.get_vizinho_direita(address) + else: + neighbor_address = neighbors.get_vizinho_esquerda(address) + + neighbor = self.router_matrix.get_router_by_address(neighbor_address) + if neighbor is not None: + neighbor.paint_arrow(neighbor_arrow) + + elif port in (MPSoCConfig.LOCAL0, MPSoCConfig.LOCAL1): + # Pacote novo saindo do PE: apaga o caminho anterior se nenhum + # outro pacote estiver em trânsito + if not self.unfinished_packets: + self.router_matrix.reset_routers() + + local_arrow = MPSoCConfig.LOCAL_IN + + # Destinos fora da malha (ex.: periféricos) nunca "chegam", então não são aguardados + if 0 <= packet.target_router < self.mpsoc_config.get_pe_number(): + self.unfinished_packets.append(packet.target_router) + else: + return + + router.paint_arrow(local_arrow) + + # Pacote chegou ao destino + if address == packet.target_router: + router.paint_arrow(MPSoCConfig.LOCAL_OUT) + if address in self.unfinished_packets: + self.unfinished_packets.remove(address) + + # ========================================== + # TAXA DE USO DOS ENLACES + # ========================================== + def checkpoint_reached(self): + """Atualiza o percentual de uso dos enlaces de todos os roteadores.""" + max_throughput_per_window = self.checkpoint.get_window_size_in_ns() / self.mpsoc_config.clock_period_in_ns + + for address in range(self.mpsoc_config.get_pe_number()): + router_info = self.mpsoc_information.get_router_information(address) + self.update_router_throughput(address, router_info, max_throughput_per_window) + + def update_router_throughput(self, address, router_info, max_throughput): + router = self.router_matrix.get_router_by_address(address) + if router is None or router_info is None: + return + + neighbors = self.mpsoc_information.neighbors + percent = 100.0 / max_throughput + + # Só as portas que possuem vizinho, mais as portas locais + ports = [] + if neighbors.get_x_coordinate(address) != 0: + ports += [MPSoCConfig.WEST0, MPSoCConfig.WEST1] + if neighbors.get_x_coordinate(address) != self.mpsoc_config.mpsoc_x - 1: + ports += [MPSoCConfig.EAST0, MPSoCConfig.EAST1] + if neighbors.get_y_coordinate(address) != 0: + ports += [MPSoCConfig.SOUTH0, MPSoCConfig.SOUTH1] + if neighbors.get_y_coordinate(address) != self.mpsoc_config.mpsoc_y - 1: + ports += [MPSoCConfig.NORTH0, MPSoCConfig.NORTH1] + ports += [MPSoCConfig.LOCAL0, MPSoCConfig.LOCAL1] + + for port in ports: + router.update_throughput(port, router_info.get_port_bandwidth_throughput_in_cycles(port) * percent) + + router_info.reset_bandwidth_throughput() + router_info.reset_throughput() diff --git a/theme.py b/theme.py new file mode 100644 index 0000000..8a71817 --- /dev/null +++ b/theme.py @@ -0,0 +1,91 @@ +""" +Temas claro/escuro da aplicação. + +Concentra as paletas do Qt e as cores da grade de roteadores, para que a +janela principal, as janelas secundárias e a matriz mudem juntas. A escolha +do usuário fica salva com QSettings e é restaurada na próxima execução. +""" + +from dataclasses import dataclass + +from PySide6.QtCore import QSettings +from PySide6.QtGui import QColor, QPalette, Qt +from PySide6.QtWidgets import QApplication + +_SETTINGS_ORG = "GAPH" +_SETTINGS_APP = "MemphisGraphicalDebugger" +_SETTINGS_KEY = "theme/dark_mode" + + +@dataclass(frozen=True) +class MatrixColors: + """Cores da área onde a grade de roteadores é desenhada.""" + + canvas: str # fundo atrás dos roteadores + text: str # textos fora do corpo do roteador (ex.: % da porta local) + cluster_border: str # borda entre clusters + + +LIGHT_MATRIX = MatrixColors(canvas="#ffffff", text="#000000", cluster_border="#808080") +DARK_MATRIX = MatrixColors(canvas="#262626", text="#e6e6e6", cluster_border="#6e6e6e") + + +def matrix_colors(dark): + return DARK_MATRIX if dark else LIGHT_MATRIX + + +def _build_palette(dark): + palette = QPalette() + + if dark: + window, base, alternate = QColor(53, 53, 53), QColor(35, 35, 35), QColor(45, 45, 45) + text, disabled_text = QColor(Qt.GlobalColor.white), QColor(127, 127, 127) + tooltip_base, highlighted_text = QColor(25, 25, 25), QColor(Qt.GlobalColor.black) + button, placeholder = QColor(53, 53, 53), QColor(150, 150, 150) + else: + window, base, alternate = QColor(240, 240, 240), QColor(Qt.GlobalColor.white), QColor(225, 225, 225) + text, disabled_text = QColor(Qt.GlobalColor.black), QColor(160, 160, 160) + tooltip_base, highlighted_text = QColor(Qt.GlobalColor.white), QColor(Qt.GlobalColor.white) + button, placeholder = QColor(240, 240, 240), QColor(120, 120, 120) + + role = QPalette.ColorRole + palette.setColor(role.Window, window) + palette.setColor(role.WindowText, text) + palette.setColor(role.Base, base) + palette.setColor(role.AlternateBase, alternate) + palette.setColor(role.ToolTipBase, tooltip_base) + palette.setColor(role.ToolTipText, text) + palette.setColor(role.Text, text) + palette.setColor(role.PlaceholderText, placeholder) + palette.setColor(role.Button, button) + palette.setColor(role.ButtonText, text) + palette.setColor(role.BrightText, QColor(Qt.GlobalColor.red)) + palette.setColor(role.Link, QColor(42, 130, 218)) + palette.setColor(role.Highlight, QColor(42, 130, 218)) + palette.setColor(role.HighlightedText, highlighted_text) + + # Sem isso, botões e campos desabilitados ficam com o texto quase + # invisível no modo escuro (o Fusion usa a cor ativa escurecida) + disabled = QPalette.ColorGroup.Disabled + for disabled_role in (role.WindowText, role.Text, role.ButtonText): + palette.setColor(disabled, disabled_role, disabled_text) + palette.setColor(disabled, role.Base, window) + palette.setColor(disabled, role.Button, window) + + return palette + + +def apply_theme(dark, app=None): + """Aplica a paleta clara ou escura na aplicação inteira.""" + app = app or QApplication.instance() + app.setStyle("Fusion") + app.setPalette(_build_palette(dark)) + + +def load_dark_mode(): + """Retorna o último tema escolhido (claro por padrão).""" + return QSettings(_SETTINGS_ORG, _SETTINGS_APP).value(_SETTINGS_KEY, False, type=bool) + + +def save_dark_mode(dark): + QSettings(_SETTINGS_ORG, _SETTINGS_APP).setValue(_SETTINGS_KEY, dark) From 3f5a61f9dc7bad047e0d39d75c2755aec0a52f8b Mon Sep 17 00:00:00 2001 From: DrakRosmann Date: Fri, 25 Sep 2026 13:23:01 -0300 Subject: [PATCH 3/4] =?UTF-8?q?Implementa=C3=A7=C3=A3o=20das=20fun=C3=A7?= =?UTF-8?q?=C3=B5es=20restantes=20dos=20menus=20e=20janelas=20do=20roteado?= =?UTF-8?q?r.=20[feature]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Router Information (clique no roteador): abas Log, Applications, Scheduling e Traffic, janela Task Information e gráfico de escalonamento - Edit: Platform Setup (flit size, clock e janela de checkpoint) e Print Router Total Link Usage - Filters: Service and PE Filter (Only/Except por serviço e filtro por PE) - File: Save/Open/Delete Project (.hdf), atalhos do Java e correção do texto "Reset Graphical Path" - Help: About e Packet Format - Documentação atualizada em documentation/implementacoes.md --- documentation/implementacoes.md | 6 +- filter_window.py | 190 +++++++ help_dialogs.py | 84 +++ main.py | 243 ++++++++- platform_setup.py | 73 +++ projects.py | 55 ++ router_info_window.py | 941 ++++++++++++++++++++++++++++++++ simulation.py | 15 +- 8 files changed, 1585 insertions(+), 22 deletions(-) create mode 100644 filter_window.py create mode 100644 help_dialogs.py create mode 100644 platform_setup.py create mode 100644 projects.py create mode 100644 router_info_window.py diff --git a/documentation/implementacoes.md b/documentation/implementacoes.md index 844d72e..90d4401 100644 --- a/documentation/implementacoes.md +++ b/documentation/implementacoes.md @@ -44,6 +44,11 @@ diretório debug/ | `simulation.py` | Controle da simulação e janelas de checkpoint | `MainFrame.java` (parte de simulação), `CheckpointController.java` | | `overview_windows.py` | Janelas Communication Overview, Task Mapping Overview e Message Log, atualizadas durante a simulação | `CommunicationOverview.java`, `TaskMappingFrame.java`, `MainFrame.showMessageList` | | `slave_matrix.py` / `pe_matrix.py` | Células das grades das duas janelas acima | — | +| `router_info_window.py` | Janela "Router Information" (clique num roteador): abas Log, Applications, Scheduling e Traffic, janela Task Information e gráfico de escalonamento | `RouterInfoFrame.java`, `PETextLog.java`, `SchedulingTab.java`, `scheduling/SchedulingPanel.java`, `TaskInfoFrame.java` | +| `platform_setup.py` | Janela Platform Setup (menu Edit): flit size, período de clock e largura da janela de checkpoint | `PlatformSetupFrame.java` | +| `filter_window.py` | Janela Service and PE Filter (menu Filters, Ctrl+F): escolhe quais pacotes a simulação processa, por serviço (Only/Except) e por PE | `FilterForm.java` | +| `projects.py` | Projetos salvos (`.hdf`): File → Save / Open / Delete Project | `MainFrame.java` (save/open/deleteMenuItem) | +| `help_dialogs.py` | Help → About e Help → Packet Format | `util/AboutFrame.java`, `MainFrame.packetFormatMenuItemActionPerformed` | | `deloream.py` | Deloream: leitor das mensagens `$$$` das tarefas nos logs dos processadores | `deloream/DeloreamMainFrame.java`, `deloream/TaskMessage.java` | | `theme.py` | Paletas dos temas claro/escuro, cores da grade e preferência salva | — | | `router_matrix.py` | Desenho da grade e de cada roteador (imagens, setas, %) | `Roteador.java`, `UJPanelImagem.java`, `MainFrame.createNoCPanel` | @@ -485,7 +490,6 @@ Classes/funções: `TaskMessage.parse(line)`, `read_name_relations(platform_path - **Back To** só vai até tempos já simulados (mesmo comportamento do Java). - **Filtro de serviços/PEs** (`Filters → Service and PE Filter`): ainda não implementado. O controlador já aceita `SimulationController.packet_filter`. -- **Janela de informações do roteador** (`RouterInfoFrame`): o `RouterWidget` já emite `clicked(router_address)`, falta a janela. - O relatório de uso de link por roteador (`printRouterTotalLinkUsage`, oculto no Java) não foi portado. --- diff --git a/filter_window.py b/filter_window.py new file mode 100644 index 0000000..067d140 --- /dev/null +++ b/filter_window.py @@ -0,0 +1,190 @@ +""" +Janela "Service and PE Filter" (menu Filters, Ctrl+F). + +Refatoração de FilterForm.java do GraphicalDebugger original. Escolhe quais +pacotes do traffic_router.txt a simulação processa: + + - aba Services: só os serviços da lista ("Only") ou todos menos eles ("Except") + - aba PE: só os pacotes capturados nos roteadores da lista + +A lista montada numa aba só passa a valer ao clicar em "Apply" (lista vazia +desliga o filtro daquela aba). Pacotes rejeitados não atualizam os roteadores +nem são desenhados; ver MPSoCInformation.get_next_packet. +""" + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import (QComboBox, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, + QMessageBox, QPushButton, QRadioButton, QTabWidget, QVBoxLayout, QWidget) + +from util.MPSoCConfig import MPSoCConfig + +# Serviço sempre descartado pelo filtro do Java +IGNORED_SERVICE = "SLACK_TIME_REPORT" + + +class _FilterTab(QWidget): + """Combo de itens + lista dos adicionados, com Add / Remove / Remove All / Apply / Close.""" + + def __init__(self, caption, added_caption, items, on_apply, on_close, extra_widget=None): + super().__init__() + self.on_apply = on_apply + + self.combo = QComboBox() + for label, value in items: + self.combo.addItem(label, value) + + self.added_list = QListWidget() + + add_button = QPushButton("Add") + remove_button = QPushButton("Remove") + remove_all_button = QPushButton("Remove All") + apply_button = QPushButton("Apply") + close_button = QPushButton("Close") + add_button.clicked.connect(self.add_selected) + remove_button.clicked.connect(self.remove_selected) + remove_all_button.clicked.connect(self.added_list.clear) + apply_button.clicked.connect(self._apply) + close_button.clicked.connect(on_close) + + left = QVBoxLayout() + left.addWidget(QLabel(caption)) + left.addWidget(self.combo) + left.addWidget(add_button) + left.addWidget(remove_button) + left.addWidget(remove_all_button) + if extra_widget is not None: + left.addWidget(extra_widget) + left.addStretch() + + right = QVBoxLayout() + right.addWidget(QLabel(added_caption)) + right.addWidget(self.added_list) + + lists = QHBoxLayout() + lists.addLayout(left) + lists.addLayout(right, 1) + + buttons = QHBoxLayout() + buttons.addStretch() + buttons.addWidget(apply_button) + buttons.addWidget(close_button) + + layout = QVBoxLayout(self) + layout.addLayout(lists) + layout.addLayout(buttons) + + def added_values(self): + return [self.added_list.item(row).data(Qt.ItemDataRole.UserRole) for row in range(self.added_list.count())] + + def add_selected(self): + value = self.combo.currentData() + if value is None or value in self.added_values(): + return + item = QListWidgetItem(self.combo.currentText()) + item.setData(Qt.ItemDataRole.UserRole, value) + self.added_list.addItem(item) + + def remove_selected(self): + for item in self.added_list.selectedItems(): + self.added_list.takeItem(self.added_list.row(item)) + + def _apply(self): + answer = QMessageBox.question(self, "Attention", "You are sure?") + if answer == QMessageBox.StandardButton.Yes: + self.on_apply(self.added_values()) + + +class FilterWindow(QWidget): + """ + Guarda o filtro aplicado e o expõe em filter(packet), usado como + SimulationController.packet_filter. A janela é criada uma vez por debug + carregado e apenas escondida ao fechar, mantendo as listas montadas. + """ + + def __init__(self, mpsoc_config, parent=None): + super().__init__(parent, Qt.WindowType.Window) + self.mpsoc_config = mpsoc_config + self.setWindowTitle("Service and PE Filter") + self.resize(480, 340) + + self.filtered_services = set() + self.filtered_pes = set() + self.except_services = False + self.ignored_service = mpsoc_config.get_service_value(IGNORED_SERVICE) + + services = [(mpsoc_config.get_string_service_name(service), service) + for service in mpsoc_config.service_reference] + pes = [(self._pe_label(address), address) for address in range(mpsoc_config.get_pe_number())] + + self.only_radio = QRadioButton("Only") + self.except_radio = QRadioButton("Except") + self.only_radio.setChecked(True) + mode = QWidget() + mode_layout = QHBoxLayout(mode) + mode_layout.setContentsMargins(0, 0, 0, 0) + mode_layout.addWidget(self.only_radio) + mode_layout.addWidget(self.except_radio) + mode_layout.addStretch() + + self.services_tab = _FilterTab("Services", "Added Services", services, + self._apply_services, self.close, mode) + self.pe_tab = _FilterTab("PEs", "Added PEs", pes, self._apply_pes, self.close) + + self.status_label = QLabel() + self.status_label.setWordWrap(True) + + tabs = QTabWidget() + tabs.addTab(self.services_tab, "Services") + tabs.addTab(self.pe_tab, "PE") + + layout = QVBoxLayout(self) + layout.addWidget(tabs) + layout.addWidget(self.status_label) + self._update_status() + + def _pe_label(self, address): + if self.mpsoc_config.router_addressing == MPSoCConfig.XY: + return self.mpsoc_config.ham_address_to_xy_label(address) + return str(address) + + # ------------------------------------------ + # Apply + # ------------------------------------------ + def _apply_services(self, services): + self.filtered_services = set(services) + self.except_services = self.except_radio.isChecked() + self._update_status() + + def _apply_pes(self, pes): + self.filtered_pes = set(pes) + self._update_status() + + def _update_status(self): + config = self.mpsoc_config + parts = [] + if self.filtered_services: + names = ", ".join(sorted(config.get_string_service_name(service) for service in self.filtered_services)) + parts.append(f"{'Except' if self.except_services else 'Only'} services: {names}") + if self.filtered_pes: + names = ", ".join(self._pe_label(address) for address in sorted(self.filtered_pes)) + parts.append(f"Only PEs: {names}") + self.status_label.setText("Active filter — " + "; ".join(parts) if parts else "No filter applied") + + # ------------------------------------------ + # Filtro + # ------------------------------------------ + def filter(self, packet): + """True se o pacote deve ser processado pela simulação.""" + if packet.service == self.ignored_service: + return False + + if self.filtered_services: + if (packet.service in self.filtered_services) == self.except_services: + return False + + # No Java o filtro de PE era ignorado quando havia filtro de serviço; + # aqui os dois valem juntos + if self.filtered_pes and packet.router_address not in self.filtered_pes: + return False + + return True diff --git a/help_dialogs.py b/help_dialogs.py new file mode 100644 index 0000000..c038269 --- /dev/null +++ b/help_dialogs.py @@ -0,0 +1,84 @@ +""" +Janelas do menu Help: "About" (util/AboutFrame.java) e "Packet Format" +(MainFrame.packetFormatMenuItemActionPerformed). +""" + +import os + +from PySide6.QtCore import Qt +from PySide6.QtGui import QFont, QPixmap +from PySide6.QtWidgets import QDialog, QDialogButtonBox, QLabel, QMessageBox, QVBoxLayout + +ICON_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icon") + +# Formato de cada linha do traffic_router.txt, como lido em information/read_traffic_data.py +PACKET_FORMAT = """\ +

Each line of traffic_router.txt is a packet header captured by a router, with tab-separated fields:

+

Time | Current Router Address | Service | Payload size | Packet bandwidth | Input port | +Target Router | Source task <optional> | Target task <optional>

+

Field description:

+
    +
  • Time: integer decimal value, the tick counter when the header entered the router by the input port.
  • +
  • Current Router Address: integer decimal value. With XY addressing, bits 0-7 (0xFF) are the +Y address and bits 8-15 (0xFF00) the X address; with Hamiltonian addressing, it is the router number.
  • +
  • Service: integer decimal value with the packet service (see services.cfg).
  • +
  • Payload size: integer decimal value, number of payload flits (the header adds 1 flit).
  • +
  • Packet bandwidth: integer decimal value, clock cycles from the moment the header enters the +router until the last flit of the packet leaves it.
  • +
  • Input port: integer decimal value, the port where the packet entered the router: +0-EAST 0, 1-EAST 1, 2-WEST 0, 3-WEST 1, 4-NORTH 0, 5-NORTH 1, 6-SOUTH 0, 7-SOUTH 1, 8-LOCAL 0, 9-LOCAL 1. +With channel_number 1 the port is 0-EAST, 1-WEST, 2-NORTH, 3-SOUTH, 4-LOCAL.
  • +
  • Target Router: same format as Current Router Address; the address in the header, i.e. the +router the packet is going to.
  • +
  • Source task: OPTIONAL, integer decimal ID of the producer task of TASK_ALLOCATION, +TASK_TERMINATED, MESSAGE_REQUEST and MESSAGE_DELIVERY packets.
  • +
  • Target task: OPTIONAL, same format as Source task, with the consumer task of those packets.
  • +
+

Original author: Marcelo Ruaro (mceloruaro@gmail.com)

+""" + + +def show_packet_format(parent): + box = QMessageBox(parent) + box.setWindowTitle("Packet Format") + box.setIcon(QMessageBox.Icon.Information) + box.setTextFormat(Qt.TextFormat.RichText) + box.setText(PACKET_FORMAT) + box.exec() + + +class AboutDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("About") + + logo = QLabel() + pixmap = QPixmap(os.path.join(ICON_DIR, "gaph_logo.png")) + if not pixmap.isNull(): + logo.setPixmap(pixmap.scaledToWidth(min(pixmap.width(), 260), + Qt.TransformationMode.SmoothTransformation)) + logo.setAlignment(Qt.AlignmentFlag.AlignCenter) + + title = QLabel("GAPH") + title_font = QFont() + title_font.setPointSize(20) + title_font.setBold(True) + title.setFont(title_font) + + lines = [title, + QLabel("Grupo de Apoio de Pesquisa ao Hardware"), + QLabel("Pontifical Catholic University of Rio Grande do Sul (PUCRS)"), + QLabel("Support contact: fernando.moraes@pucrs.br"), + QLabel("Porto Alegre - Brasil"), + QLabel("Memphis-V Graphical Debugger — Python/PySide6 port")] + + buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) + buttons.rejected.connect(self.reject) + + layout = QVBoxLayout(self) + layout.addWidget(logo) + for label in lines: + label.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(label) + layout.addWidget(buttons) + layout.setSizeConstraint(QVBoxLayout.SizeConstraint.SetFixedSize) diff --git a/main.py b/main.py index 8063286..24fce8b 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ import sys from PySide6.QtGui import Qt, QPalette, QColor, QAction, QKeySequence -from PySide6.QtWidgets import (QApplication, QMainWindow, QFileDialog, QMessageBox, QWidget, +from PySide6.QtWidgets import (QApplication, QMainWindow, QFileDialog, QMessageBox, QWidget, QInputDialog, QVBoxLayout, QHBoxLayout, QScrollArea, QTableWidgetItem, QTableWidget, QLabel, QHeaderView) @@ -14,6 +14,11 @@ from simulation import SimulationController, MAX_REPAINT_SPEED, ticks_to_ms from overview_windows import CommunicationOverviewWindow, MessageLogWindow, TaskMappingWindow from deloream import DeloreamWindow +from router_info_window import RouterInfoWindow +from platform_setup import PlatformSetupDialog +from filter_window import FilterWindow +from help_dialogs import AboutDialog, show_packet_format +import projects class MinhaJanela(QMainWindow, MainWindow): @@ -27,6 +32,11 @@ class MinhaJanela(QMainWindow, MainWindow): task_mapping_window = None message_log_window = None deloream_window = None + router_info_windows = None + simulation_ticks = 0 + filter_window = None + + WINDOW_TITLE = "Memphis-V Graphical Debugger" # Arquivos obrigatórios em um diretório de debug REQUIRED_FILES = ("platform.cfg", "services.cfg", "traffic_router.txt") @@ -37,6 +47,7 @@ class MinhaJanela(QMainWindow, MainWindow): def __init__(self): super().__init__() self.setupUi(self) + self.setWindowTitle(self.WINDOW_TITLE) # --------------------------------------------------------- # SISTEMA DE TEMA (DARK/LIGHT MODE) @@ -61,6 +72,23 @@ def __init__(self): self.actionMessage_Log.setEnabled(False) self.actionTask_List.setEnabled(False) + # Atalhos do menu File iguais aos do Java (o .ui só define Ctrl+N e Ctrl+S) + self.actionRest_Graphical_Path.setText("Reset Graphical Path") + for action, shortcut in ((self.actionOpen_Project, "Ctrl+O"), (self.actionDelete_Project, "Ctrl+D"), + (self.actionReset_Simulation, "Ctrl+R"), (self.actionRest_Graphical_Path, "Ctrl+G"), + (self.actionExit, "Ctrl+Q")): + action.setShortcut(QKeySequence(shortcut)) + + self.actionSave_Project.triggered.connect(self.save_project) + self.actionOpen_Project.triggered.connect(self.open_project) + self.actionDelete_Project.triggered.connect(self.delete_project) + self.actionAbout.triggered.connect(lambda: AboutDialog(self).exec()) + self.actionPacket_Format.triggered.connect(lambda: show_packet_format(self)) + + self.actionPrint_Link_Usage = QAction("Print Router Total Link Usage", self) + self.menuEdit.addAction(self.actionPrint_Link_Usage) + self.actionPrint_Link_Usage.triggered.connect(self.configure_link_usage_print) + self.actionExit.triggered.connect(self.close) self.actionNew_Debugging.triggered.connect(self.open_file) self.horizontalSlider.valueChanged.connect(self.update_slide) @@ -71,6 +99,9 @@ def __init__(self): self.actionServices_List.triggered.connect(self.servicesList) self.actionMessage_Log.triggered.connect(self.open_message_log) self.actionDeloream.triggered.connect(self.open_deloream) + self.actionPlatform_Setup.triggered.connect(self.open_platform_setup) + self.actionService_and_PE_Filter.setShortcut(QKeySequence("Ctrl+F")) + self.actionService_and_PE_Filter.triggered.connect(self.open_filter) # --------------------------------------------------------- # CONTROLE DA SIMULAÇÃO @@ -157,6 +188,24 @@ def open_deloream(self): self.deloream_window.raise_() self.deloream_window.activateWindow() + def open_platform_setup(self): + if self.simulation is None: + QMessageBox.warning(self, "Attention", "Please, load a debugging before") + return + if PlatformSetupDialog(self.mpconfig, self.simulation.checkpoint, self).exec(): + # O tempo em ms depende do período de clock + self.update_simulation_time(self.simulation_ticks) + + def open_filter(self): + if self.simulation is None: + QMessageBox.warning(self, "Attention", "Please, load a debugging before") + return + # Como no Java, apaga o caminho desenhado antes de trocar o filtro + self.simulation.reset_graphical_path() + self.filter_window.show() + self.filter_window.raise_() + self.filter_window.activateWindow() + def open_message_log(self): if self.filePath == "": QMessageBox.warning(self, "Attention", "Please, load a debugging before") @@ -179,6 +228,27 @@ def open_taskmap(self): self.task_mapping_window.raise_() self.task_mapping_window.activateWindow() + def open_router_info(self, router_address): + """Abre a janela "Router Information" do roteador clicado (uma por roteador).""" + if self.router_info_windows is None: + self.router_info_windows = {} + + window = self.router_info_windows.get(router_address) + if window is None: + window = RouterInfoWindow(self.mpconfig, self.mpsoc_information, router_address, self.simulation) + window.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) + window.destroyed.connect(lambda *_, address=router_address: + self.router_info_windows.pop(address, None)) + self.router_info_windows[router_address] = window + window.show() + window.raise_() + window.activateWindow() + + def close_router_info_windows(self): + windows = self.router_info_windows or {} + while windows: + windows.popitem()[1].close() + def update_slide(self, valor): self.label_2.setText(str(valor)) if self.simulation is not None: @@ -186,9 +256,11 @@ def update_slide(self, valor): def open_file(self): caminho = QFileDialog.getExistingDirectory(self, "Open Debug Directory", "./") - if caminho == "": - return + if caminho != "": + self.load_debug(caminho) + def load_debug(self, caminho, title=None): + """Carrega um diretório de debug. Retorna False se ele não for válido.""" # Verifica se o diretório possui todos os arquivos necessários faltando = [arquivo for arquivo in self.REQUIRED_FILES if not os.path.isfile(os.path.join(caminho, arquivo))] @@ -199,25 +271,143 @@ def open_file(self): "O diretório selecionado não é um diretório de debug valido.\n\n" "Selecione um diretório valido" ) - return + return False self.filePath = caminho - if self.filePath != "": - self.actionCommunication_Overview.setEnabled(True) - self.actionDeloream.setEnabled(True) - self.actionTask_Mapping_Overview.setEnabled(True) - self.actionServices_List.setEnabled(True) - self.actionMessage_Log.setEnabled(True) - self.actionTask_List.setEnabled(True) - - self.mpconfig = MPSoCConfig.MPSoCConfig(self.filePath) - - # Fecha o traffic_router.txt do debug anterior antes de abrir o novo - if self.mpsoc_information is not None: - self.mpsoc_information.close() - self.mpsoc_information = MPSoCInformation(self.mpconfig) - self.build_router_matrix() - self.start_simulation() + self.actionCommunication_Overview.setEnabled(True) + self.actionDeloream.setEnabled(True) + self.actionTask_Mapping_Overview.setEnabled(True) + self.actionServices_List.setEnabled(True) + self.actionMessage_Log.setEnabled(True) + self.actionTask_List.setEnabled(True) + + self.mpconfig = MPSoCConfig.MPSoCConfig(self.filePath) + + # Fecha o traffic_router.txt do debug anterior antes de abrir o novo + if self.mpsoc_information is not None: + self.mpsoc_information.close() + self.mpsoc_information = MPSoCInformation(self.mpconfig) + self.build_router_matrix() + self.start_simulation() + + self.setWindowTitle(f"{self.WINDOW_TITLE}: {title or self.mpconfig.get_testcase_path() or caminho}") + return True + + # ========================================== + # PROJETOS (.hdf) + # ========================================== + def save_project(self): + if self.mpconfig is None: + QMessageBox.warning(self, "Attention", "Please, load a debugging before") + return + + name = "" + while True: + name, ok = QInputDialog.getText(self, "Save Project", "Inform the debugging project name", text=name) + if not ok: + return + name = name.strip() + error = projects.validate_name(name) + if error is None: + break + QMessageBox.critical(self, "Error", error) + + if os.path.exists(projects.project_path(name)): + answer = QMessageBox.question(self, "Save Project", f"Project {name} already exists. Overwrite it?") + if answer != QMessageBox.StandardButton.Yes: + return + + try: + projects.save_project(name, self.filePath) + except OSError as error: + QMessageBox.critical(self, "Save Project", f"Could not save the project {name}:\n{error}") + return + self.setWindowTitle(f"{self.WINDOW_TITLE}: {name}") + self.statusbar.showMessage(f"Project saved in {projects.project_path(name)}", 5000) + + def _choose_project(self, title, accept_label=None): + dialog = QFileDialog(self, title, projects.projects_dir(), projects.PROJECT_FILTER) + dialog.setFileMode(QFileDialog.FileMode.ExistingFile) + if accept_label is not None: + dialog.setLabelText(QFileDialog.DialogLabel.Accept, accept_label) + if dialog.exec() and dialog.selectedFiles(): + return dialog.selectedFiles()[0] + return None + + def open_project(self): + path = self._choose_project("Open Project") + if path is None: + return + try: + debug_dir = projects.read_project(path) + except OSError: + QMessageBox.critical(self, "Error", "File not exists") + return + + if not os.path.isdir(debug_dir): + QMessageBox.critical(self, "Open Project", + f"The debug directory of project {projects.project_name(path)} no longer exists:\n" + f"{debug_dir or '(empty file)'}") + return + self.stop_simulation() + self.load_debug(debug_dir, projects.project_name(path)) + + def delete_project(self): + path = self._choose_project("Delete Project", "Delete") + if path is None: + return + name = projects.project_name(path) + answer = QMessageBox.question(self, "Delete Project", f"Delete the project {name}?") + if answer != QMessageBox.StandardButton.Yes: + return + try: + os.remove(path) + except OSError: + QMessageBox.warning(self, "Attention", f"Error during the deletion of project: {name}") + return + QMessageBox.information(self, "Delete Project", f"Project {name} successfully deleted!") + + def configure_link_usage_print(self): + """ + "Print Router Total Link Usage" do Java: a cada checkpoint imprime no + terminal a soma do uso dos enlaces do roteador escolhido (ou o tempo). + """ + if self.simulation is None: + QMessageBox.warning(self, "Attention", "Please, load a debugging before") + return + + current = self.simulation.link_usage_router + default = "" if current < 0 else self._router_label(current) + label, ok = QInputDialog.getText(self, "Print Router Total Link Usage", + "Inform the router address (empty to stop printing)", text=default) + if not ok: + return + label = label.strip() + if label == "": + self.simulation.link_usage_router = -1 + return + try: + if self.mpconfig.router_addressing == MPSoCConfig.MPSoCConfig.XY: + address = self.mpconfig.xy_label_to_ham_address(label) + else: + address = int(label) + if not 0 <= address < self.mpconfig.get_pe_number(): + raise ValueError + except ValueError: + QMessageBox.warning(self, "Attention", f"Invalid router address: {label}") + return + + mode, ok = QInputDialog.getItem(self, "Print Router Total Link Usage", "Inform the printing type:", + ["1 - Link Usage", "2 - Time"], 0, False) + if not ok: + return + self.simulation.link_usage_router = address + self.simulation.print_link_usage = mode.startswith("1") + + def _router_label(self, address): + if self.mpconfig.router_addressing == MPSoCConfig.MPSoCConfig.XY: + return self.mpconfig.ham_address_to_xy_label(address) + return str(address) def build_router_matrix(self): """ @@ -228,6 +418,9 @@ def build_router_matrix(self): if self.mpconfig is None: return + # Janelas de roteador pertencem ao debug anterior + self.close_router_info_windows() + # Cria a matriz de roteadores com base na configuração carregada self.router_matrix_widget = RouterMatrixWidget( self.mpconfig.mpsoc_x, @@ -237,6 +430,8 @@ def build_router_matrix(self): mpsoc_config=self.mpconfig, ) self.router_matrix_widget.apply_theme(self.is_dark_mode) + for router in self.router_matrix_widget.routers.values(): + router.clicked.connect(self.open_router_info) # Primeira vez: cria um QScrollArea dentro do self.frame para # comportar malhas maiores que a área visível da janela. @@ -280,6 +475,13 @@ def start_simulation(self): self.router_matrix_widget, self) self.simulation.set_speed(self.horizontalSlider.value()) + # Filtro de serviços/PEs: um por debug carregado (as listas voltam a vazio) + if self.filter_window is not None: + self.filter_window.close() + self.filter_window.deleteLater() + self.filter_window = FilterWindow(self.mpconfig, self) + self.simulation.packet_filter = self.filter_window.filter + # Janelas de visão geral abertas passam a acompanhar o novo debug for window in (self.communication_window, self.task_mapping_window, self.message_log_window): if window is not None and window.isVisible(): @@ -409,6 +611,7 @@ def build_speed_control(self): self.horizontalLayout_4.addLayout(speed_layout) def update_simulation_time(self, ticks): + self.simulation_ticks = ticks clock_period = self.mpconfig.clock_period_in_ns if self.mpconfig else 0 self.simulation_time_label.setText(f"{ticks_to_ms(ticks, clock_period):.5f} ms") self.simulation_ticks_label.setText(f"{ticks} ticks") diff --git a/platform_setup.py b/platform_setup.py new file mode 100644 index 0000000..bb96e7d --- /dev/null +++ b/platform_setup.py @@ -0,0 +1,73 @@ +""" +Janela "Platform Setup" (menu Edit). + +Refatoração de PlatformSetupFrame.java do GraphicalDebugger original: permite +trocar o tamanho do flit, o período de clock e a largura da janela de +checkpoint (intervalo em que a % de uso dos enlaces é recalculada) sem +editar o platform.cfg. As mudanças valem até o próximo "Reset Simulation" +ou "New Debugging", que releem os arquivos. +""" + +from PySide6.QtCore import QLocale +from PySide6.QtGui import QDoubleValidator, QIntValidator +from PySide6.QtWidgets import QDialog, QDialogButtonBox, QGridLayout, QLabel, QLineEdit, QMessageBox, QVBoxLayout + + +class PlatformSetupDialog(QDialog): + def __init__(self, mpsoc_config, checkpoint, parent=None): + super().__init__(parent) + self.mpsoc_config = mpsoc_config + self.checkpoint = checkpoint + self.setWindowTitle("Platform Setup") + + self.flit_size_edit = QLineEdit(str(mpsoc_config.flit_size)) + self.flit_size_edit.setValidator(QIntValidator(1, 1024, self)) + + self.clock_edit = QLineEdit(str(mpsoc_config.clock_period_in_ns)) + self.clock_edit.setValidator(QIntValidator(1, 1_000_000, self)) + + # Largura da janela em us, como no Java (guardada em ms no controlador) + self.window_width_edit = QLineEdit(f"{checkpoint.window_size_ms * 1000:g}") + window_validator = QDoubleValidator(0.001, 1e9, 3, self) + window_validator.setNotation(QDoubleValidator.Notation.StandardNotation) + window_validator.setLocale(QLocale.c()) # aceita ponto decimal em qualquer idioma + self.window_width_edit.setValidator(window_validator) + + form = QGridLayout() + rows = (("Flit Size", self.flit_size_edit, "bits"), + ("Clock Period", self.clock_edit, "ns"), + ("Window Width", self.window_width_edit, "us")) + for row, (caption, edit, unit) in enumerate(rows): + form.addWidget(QLabel(caption), row, 0) + form.addWidget(edit, row, 1) + form.addWidget(QLabel(unit), row, 2) + + buttons = QDialogButtonBox() + buttons.addButton("Confirm", QDialogButtonBox.ButtonRole.AcceptRole) + buttons.addButton("Cancel", QDialogButtonBox.ButtonRole.RejectRole) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + + layout = QVBoxLayout(self) + layout.addLayout(form) + layout.addWidget(buttons) + layout.setSizeConstraint(QVBoxLayout.SizeConstraint.SetFixedSize) # não redimensionável, como no Java + + def accept(self): + edits = (self.flit_size_edit, self.clock_edit, self.window_width_edit) + invalid = next((edit for edit in edits if not edit.hasAcceptableInput()), None) + if invalid is not None: + QMessageBox.warning(self, "Platform Setup", "Enter only valid positive numbers.") + invalid.setFocus() + invalid.selectAll() + return + + clock_period = int(self.clock_edit.text()) + self.mpsoc_config.flit_size = int(self.flit_size_edit.text()) + self.mpsoc_config.clock_period_in_ns = clock_period + # O Java esquecia de repassar o clock ao CheckpointController + self.checkpoint.clock_period_ns = clock_period + self.checkpoint.window_size_ms = float(self.window_width_edit.text()) / 1000.0 + + QMessageBox.information(self, "Platform Setup", "Settings applied") + super().accept() diff --git a/projects.py b/projects.py new file mode 100644 index 0000000..60067db --- /dev/null +++ b/projects.py @@ -0,0 +1,55 @@ +""" +Projetos de debug salvos (File -> Save / Open / Delete Project). + +Como no Java (MainFrame.saveMenuItemActionPerformed e afins), um projeto é +um arquivo .hdf ("HeMPS Debugging File") cuja primeira linha é o +caminho do diretório de debug. O Java gravava em /projects, +que muda conforme de onde o programa é executado (e não existe no +executável do PyInstaller); aqui a pasta fica nos dados do usuário. +""" + +import os + +from PySide6.QtCore import QStandardPaths + +PROJECT_EXTENSION = ".hdf" +PROJECT_FILTER = "HeMPS Debugging File (*.hdf)" + + +def projects_dir(): + """Pasta dos projetos, criada se ainda não existir.""" + # Local fixo, independente do nome dado ao QApplication (~/.local/share no Linux) + base = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.GenericDataLocation) + path = os.path.join(base, "GAPH", "MemphisGraphicalDebugger", "projects") + os.makedirs(path, exist_ok=True) + return path + + +def project_path(name): + return os.path.join(projects_dir(), name + PROJECT_EXTENSION) + + +def validate_name(name): + """Mensagem de erro para o nome informado, ou None se ele for válido.""" + if not name: + return "Name is empty" + if " " in name: + return "Name contains space" + if os.sep in name or (os.altsep and os.altsep in name) or name in (".", ".."): + return "Name contains invalid characters" + return None + + +def save_project(name, debug_dir): + with open(project_path(name), "w", encoding="utf-8") as project: + project.write(os.path.abspath(debug_dir) + "\n") + + +def read_project(path): + """Diretório de debug guardado no projeto (primeira linha do arquivo).""" + with open(path, "r", encoding="utf-8") as project: + return project.readline().strip() + + +def project_name(path): + return os.path.splitext(os.path.basename(path))[0] diff --git a/router_info_window.py b/router_info_window.py new file mode 100644 index 0000000..0519b39 --- /dev/null +++ b/router_info_window.py @@ -0,0 +1,941 @@ +""" +Janela "Router Information", aberta ao clicar em um roteador da malha. + +Refatoração de RouterInfoFrame.java, PETextLog.java, SchedulingTab.java, +scheduling/SchedulingPanel.java, TaskInfoFrame.java e dos renderizadores +RouterInfoTableCellRender/CellRenderer do GraphicalDebugger original. + +Abas da janela (mesma ordem do Java): + - Log: log do processador (log/logx.txt do cenário) + - Applications: tarefas alocadas/terminadas no PE; duplo clique abre a + janela "Task Information" + - Scheduling: gráfico de escalonamento do PE (debug/scheduling_report.txt) + - Traffic: volume por porta de entrada, com filtro de serviços +""" + +import bisect +import os +from dataclasses import dataclass + +from PySide6.QtCore import QEvent, QRect, Qt, QTimer +from PySide6.QtGui import QColor, QFont, QFontDatabase, QPainter, QPen, QPixmap +from PySide6.QtWidgets import (QAbstractItemView, QComboBox, QGridLayout, QGroupBox, QHBoxLayout, + QHeaderView, QInputDialog, QLabel, QMessageBox, QPlainTextEdit, + QPushButton, QScrollArea, QTableWidget, QTableWidgetItem, QTabWidget, + QVBoxLayout, QWidget) + +from overview_windows import _LiveOverview, _router_label +from util.MPSoCConfig import MPSoCConfig + +ICON_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icon") + +# Intervalo do "Continuous Read" do log e da atualização da Task Information +LOG_READ_INTERVAL_MS = 1000 +TASK_INFO_REFRESH_MS = 500 + +# Eventos de CPU usados quando o debug/cpu.cfg não existe (padrão do Java) +DEFAULT_CPU_EVENTS = {65536: "Interruption", 262144: "Scheduler", 524288: "Idle"} + +# Marcas da régua do gráfico de escalonamento: a cada 1, 2, 5, 10... unidades +_RULER_STEPS = [base * 10 ** power for power in range(7) for base in (1, 2, 5)] + +# Cores das células destacadas (mesmas do Java) +RUNNING_BACKGROUND = QColor(Qt.GlobalColor.blue) +CHECKED_BACKGROUND = QColor(Qt.GlobalColor.blue) +PENDING_BACKGROUND = QColor(Qt.GlobalColor.red) + + +def _read_only_table(headers): + table = QTableWidget(0, len(headers)) + table.setHorizontalHeaderLabels(headers) + table.verticalHeader().setVisible(False) + table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + return table + + +def _highlight(item, background): + item.setBackground(background) + item.setForeground(QColor(Qt.GlobalColor.white)) + + +def _task_name(mpsoc_config, task_id): + return mpsoc_config.task_name_hash.get(task_id, str(task_id)) + + +def _pe_xy_address(mpsoc_config, router_address): + """Endereço (x << 8) | y do PE, usado nos nomes dos arquivos de log.""" + return mpsoc_config.ham_to_xy_addr(router_address) + + +def _pe_report_address(mpsoc_config, router_address): + """Endereço do PE como aparece no scheduling_report.txt (XY ou hamiltoniano).""" + if mpsoc_config.router_addressing == MPSoCConfig.XY: + return _pe_xy_address(mpsoc_config, router_address) + return router_address + + +def _task_lifetime(tasks, task_id): + """(tempo de alocação, tempo de término ou None) da última execução da tarefa.""" + allocated, terminated = 0, None + for task in tasks: + if task.id != task_id: + continue + if task.service == "ALLOCATED": + allocated, terminated = task.time, None + elif task.service == "TERMINATED": + terminated = task.time + return allocated, terminated + + +# ========================================== +# JANELA PRINCIPAL DO ROTEADOR +# ========================================== +class RouterInfoWindow(_LiveOverview): + """ + Informações de um roteador/PE. Diferente do Java (que mostrava uma foto + do momento em que a janela foi aberta), as tabelas acompanham a + simulação, igual às demais janelas de visão geral. + """ + + def __init__(self, mpsoc_config, mpsoc_information, router_address, simulation=None): + self.router_address = router_address + self.task_windows = [] + super().__init__(mpsoc_config, mpsoc_information, simulation) + + self.setWindowTitle(f"Router {_router_label(mpsoc_config, router_address)} Information") + self.setMinimumSize(440, 365) + self.resize(560, 540) + + self.tabs = QTabWidget(self) + self.log_tab = PELogTab(mpsoc_config, router_address) + self.applications_tab = self._build_applications_tab() + self.scheduling_tab = SchedulingTab(mpsoc_config, router_address) + self.traffic_tab = self._build_traffic_tab() + + self.tabs.addTab(self.log_tab, "Log") + self.tabs.addTab(self.applications_tab, "Applications") + self.tabs.addTab(self.scheduling_tab, "Scheduling") + self.tabs.addTab(self.traffic_tab, "Traffic") + + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.tabs) + + self.refresh() + + @property + def router_info(self): + # Buscado a cada uso: o "Go" da simulação recria os RouterInformation + return self.mpsoc_information.get_router_information(self.router_address) + + def rebuild(self): + self.refresh() + + def refresh(self): + self._dirty = False + if not hasattr(self, "traffic_table"): + return # chamado pela base antes da interface existir + self._update_task_table() + self._update_traffic_table() + + def closeEvent(self, event): + self.log_tab.stop_continuous_read() + for window in self.task_windows: + window.close() + super().closeEvent(event) + + # ------------------------------------------ + # Aba Applications + # ------------------------------------------ + def _build_applications_tab(self): + self.task_table = _read_only_table(["Task Name", "Task", "Service", "Time"]) + self.task_table.setToolTip("Double click a task to see its messages.\n" + "ALLOCATED* = task still running") + self.task_table.cellDoubleClicked.connect(self._open_task_info) + return self.task_table + + def _update_task_table(self): + tasks = self.router_info.get_tasks_information() + terminated = {task.id for task in tasks if task.service == "TERMINATED"} + + # Primeiro as alocações, depois os términos (mesma ordem do Java) + rows = [] + for task in tasks: + if task.service == "ALLOCATED": + running = task.id not in terminated + rows.append((task, "ALLOCATED*" if running else "ALLOCATED", running)) + rows.extend((task, "TERMINATED", False) for task in tasks if task.service == "TERMINATED") + + table = self.task_table + table.setRowCount(len(rows)) + for row, (task, service, running) in enumerate(rows): + values = (_task_name(self.mpsoc_config, task.id), str(task.id), service, str(task.time)) + for column, value in enumerate(values): + item = QTableWidgetItem(value) + item.setData(Qt.ItemDataRole.UserRole, task.id) + if running and column == 2: + _highlight(item, RUNNING_BACKGROUND) + table.setItem(row, column, item) + + def _open_task_info(self, row, _column): + item = self.task_table.item(row, 0) + if item is None: + return + window = TaskInfoWindow(self.mpsoc_config, self, item.data(Qt.ItemDataRole.UserRole)) + window.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) + window.destroyed.connect(lambda *_, w=window: self._forget_task_window(w)) + self.task_windows.append(window) + window.show() + + def _forget_task_window(self, window): + if window in self.task_windows: + self.task_windows.remove(window) + + # ------------------------------------------ + # Aba Traffic + # ------------------------------------------ + def _build_traffic_tab(self): + tab = QWidget() + + filter_box = QGroupBox("Service Filter", tab) + self.services_to_add = QComboBox() + self.services_to_add.addItem("None") + for service_name in self.mpsoc_config.get_services_hash().values(): + self.services_to_add.addItem(service_name) + + self.services_added = QComboBox() + self.services_added.addItem("Show all") + self.services_added.currentIndexChanged.connect(self._update_traffic_table) + + add_button = QPushButton("Add >>") + remove_button = QPushButton("Remove <<") + add_button.clicked.connect(self._add_service) + remove_button.clicked.connect(self._remove_service) + + filter_layout = QGridLayout(filter_box) + filter_layout.addWidget(QLabel("Services to add"), 0, 0) + filter_layout.addWidget(QLabel("Added services"), 0, 2) + filter_layout.addWidget(self.services_to_add, 1, 0) + filter_layout.addWidget(add_button, 0, 1) + filter_layout.addWidget(remove_button, 1, 1) + filter_layout.addWidget(self.services_added, 1, 2) + filter_layout.setColumnStretch(0, 1) + filter_layout.setColumnStretch(2, 1) + + self.traffic_table = _read_only_table(["Input Port", "Total Volume", "Service Volume", "Percentual"]) + self.traffic_table.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection) + + layout = QVBoxLayout(tab) + layout.addWidget(filter_box) + layout.addWidget(self.traffic_table) + return tab + + def selected_services(self): + """Serviço escolhido em "Added services"; em "Show all", todos os adicionados.""" + combo = self.services_added + if combo.currentIndex() > 0: + names = [combo.currentText()] + else: + names = [combo.itemText(index) for index in range(1, combo.count())] + return [self.mpsoc_config.get_service_value(name) for name in names] + + def _add_service(self): + if self.services_to_add.currentIndex() == 0: + return + name = self.services_to_add.currentText() + if self.services_added.findText(name) == -1: + self.services_added.addItem(name) + # Volta para "Show all" (o sinal atualiza a tabela) + self.services_added.setCurrentIndex(0) + self._update_traffic_table() + + def _remove_service(self): + index = self.services_added.currentIndex() + if index > 0: + self.services_added.removeItem(index) + self._update_traffic_table() + + def _update_traffic_table(self, *_): + router = self.router_info + services = self.selected_services() + + def percent(part, total): + return f"{part * 100 // total}%" if total else "0%" + + rows = [] + router_total = router_services = 0 + for port in range(MPSoCConfig.NPORT): + total = router.get_port_total_volume_in_flits(port) + service = router.get_port_volume_in_flits(port, services) if services else 0 + router_total += total + router_services += service + rows.append((MPSoCConfig.get_port_string(port), total, service)) + rows.append(("TOTAL", router_total, router_services)) + + table = self.traffic_table + table.setRowCount(len(rows)) + bold = QFont() + bold.setBold(True) + for row, (port, total, service) in enumerate(rows): + values = (port, f"{total} flits", f"{service} flits", percent(service, total)) + for column, value in enumerate(values): + item = QTableWidgetItem(value) + if port == "TOTAL": + item.setFont(bold) + table.setItem(row, column, item) + + +# ========================================== +# ABA LOG +# ========================================== +class PELogTab(QWidget): + """Mostra o log do processador (log/logx.txt), lido uma vez ou continuamente.""" + + def __init__(self, mpsoc_config, router_address, parent=None): + super().__init__(parent) + xy = _pe_xy_address(mpsoc_config, router_address) + testcase = mpsoc_config.get_testcase_path() or os.path.dirname( + os.path.normpath(mpsoc_config.debug_file_path)) + self.log_path = os.path.join(testcase, "log", f"log{xy >> 8}x{xy & 0xFF}.txt") + + self.read_once_button = QPushButton("Read Log Once") + self.continuous_button = QPushButton("Continuous Read") + self.continuous_button.setCheckable(True) + self.read_once_button.clicked.connect(self.update_log) + self.continuous_button.toggled.connect(self._on_continuous_toggled) + + self.text = QPlainTextEdit() + self.text.setReadOnly(True) + self.text.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap) + font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont) + font.setPointSize(11) + self.text.setFont(font) + + self.timer = QTimer(self) + self.timer.timeout.connect(self.update_log) + + buttons = QHBoxLayout() + buttons.addWidget(self.read_once_button) + buttons.addWidget(self.continuous_button) + buttons.addStretch() + + layout = QVBoxLayout(self) + layout.addLayout(buttons) + layout.addWidget(self.text) + + def _on_continuous_toggled(self, checked): + font = self.continuous_button.font() + font.setBold(checked) + self.continuous_button.setFont(font) + self.continuous_button.setText("Reading..." if checked else "Continuous Read") + if checked: + self.update_log() + self.timer.start(LOG_READ_INTERVAL_MS) + else: + self.timer.stop() + + def stop_continuous_read(self): + self.continuous_button.setChecked(False) + + def update_log(self): + try: + with open(self.log_path, "r", encoding="utf-8", errors="replace") as log_file: + content = log_file.read() + except OSError: + content = f"Log file not found:\n{self.log_path}" + + if content == self.text.toPlainText(): + return # evita perder a rolagem/seleção sem necessidade + self.text.setPlainText(content) + # Mantém a rolagem no fim, como no Java + scroll = self.text.verticalScrollBar() + scroll.setValue(scroll.maximum()) + + +# ========================================== +# ABA SCHEDULING +# ========================================== +class SchedulingTab(QWidget): + """Botões que abrem o gráfico de escalonamento do PE ou de uma aplicação.""" + + def __init__(self, mpsoc_config, router_address, parent=None): + super().__init__(parent) + self.mpsoc_config = mpsoc_config + self.router_address = router_address + self.graph_windows = [] + + pe_button = QPushButton("Open Scheduling Graph") + app_button = QPushButton("Open Application Scheduling") + for button in (pe_button, app_button): + button.setMinimumWidth(220) + pe_button.clicked.connect(lambda: self._open_graph(only_app=False)) + app_button.clicked.connect(lambda: self._open_graph(only_app=True)) + + layout = QVBoxLayout(self) + layout.addStretch(1) + layout.addWidget(pe_button, 0, Qt.AlignmentFlag.AlignHCenter) + layout.addWidget(app_button, 0, Qt.AlignmentFlag.AlignHCenter) + layout.addStretch(3) + + def _open_graph(self, only_app): + app_id = -1 + if only_app: + app_id, ok = QInputDialog.getInt(self, "Application Scheduling", + "Please, inform the application ID:", 0, 0, 0xFF) + if not ok: + return + + report = os.path.join(self.mpsoc_config.debug_file_path, "scheduling_report.txt") + if not os.path.isfile(report): + QMessageBox.warning(self, "Scheduling", f"File not found:\n{report}") + return + + window = SchedulingGraphWindow(self.mpsoc_config, self.router_address, app_id) + window.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) + window.destroyed.connect(lambda *_, w=window: self.graph_windows.remove(w)) + self.graph_windows.append(window) + window.show() + + +@dataclass +class SchedulingEvent: + code: int + name: str + start: int + finish: int + rect: QRect = None # posição no último desenho, para o clique + + +def read_cpu_events(debug_dir): + """Nomes dos eventos de CPU (debug/cpu.cfg: nomecódigo).""" + events = {} + try: + with open(os.path.join(debug_dir, "cpu.cfg"), "r", encoding="utf-8") as cpu_file: + for line in cpu_file: + fields = line.split() + if len(fields) >= 2: + events[int(fields[1])] = fields[0] + except (OSError, ValueError): + events = {} + return events or dict(DEFAULT_CPU_EVENTS) + + +def read_scheduling_report(path, pe_filter=None): + """ + Lê o scheduling_report.txt (PEcódigotempo). Cada linha marca + o início de um trecho que termina na próxima linha do mesmo PE; o último + trecho de cada PE ganha 10 ticks, como no Java. + + Retorna {PE: [(código, início, fim), ...]}. + """ + slices = {} + current = {} + with open(path, "r", encoding="utf-8") as report: + for line in report: + fields = line.split("\t") + if len(fields) < 3: + continue + try: + pe, code, time = int(fields[0]), int(fields[1]), int(fields[2]) + except ValueError: + continue + if pe_filter is not None and pe != pe_filter: + continue + + if pe in current: + last_code, last_start = current[pe] + slices.setdefault(pe, []).append((last_code, last_start, time)) + current[pe] = (code, time) + + for pe, (code, start) in current.items(): + slices.setdefault(pe, []).append((code, start, start + 10)) + return slices + + +class SchedulingGraph(QWidget): + """ + Gráfico de Gantt do escalonamento (SchedulingPanel.java). + + - clique num trecho: mostra os dados dele em "Execution Slice" + - linha vermelha segue o mouse e mostra o tempo em ticks + - Ctrl + roda do mouse: zoom horizontal + """ + + TOP = 50 # INITI_Y + ROW_HEIGHT = 26 # NAME_SPACE + CPU_COLOR = QColor(21, 119, 40) + TASK_COLOR = QColor(Qt.GlobalColor.blue) + + def __init__(self, mpsoc_config, router_address, app_filter=-1, parent=None): + super().__init__(parent) + self.mpsoc_config = mpsoc_config + self.router_address = router_address + self.app_filter = app_filter + self.cpu_events = read_cpu_events(mpsoc_config.debug_file_path) + + self.events = self._load_events() + self.last_time = max((event.finish for event in self.events), default=0) + + # Uma linha por código, na ordem em que aparecem + self.rows = {} + usage = {} + for event in self.events: + if event.code not in self.rows: + self.rows[event.code] = (len(self.rows) + 1) * self.ROW_HEIGHT + usage[event.code] = usage.get(event.code, 0) + event.finish - event.start + self.usage = usage + self.total_cpu_time = sum(usage.values()) + self.names = {event.code: event.name for event in self.events} + + # Início de cada trecho, para achar o trecho clicado com bisect + self.events.sort(key=lambda event: event.start) + self.starts = [event.start for event in self.events] + + self.selected = None + self.mouse_x = None + self._cache = None + self.setMouseTracking(True) + self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + + self.time_line = len(self.rows) * self.ROW_HEIGHT + self.TOP + self.ROW_HEIGHT // 2 + self.report_top = self.time_line + 50 + report_lines = len(self.rows) + 3 if app_filter == -1 else 6 + self.setMinimumHeight(self.report_top + report_lines * 15 + 20) + + def _load_events(self): + config = self.mpsoc_config + path = os.path.join(config.debug_file_path, "scheduling_report.txt") + + if self.app_filter == -1: + pe = _pe_report_address(config, self.router_address) + slices = read_scheduling_report(path, pe).get(pe, []) + else: + # Tarefas da aplicação em todos os PEs (os eventos de CPU têm bits acima do 16) + slices = [piece for pe_slices in read_scheduling_report(path).values() + for piece in pe_slices + if piece[0] >> 16 == 0 and piece[0] >> 8 == self.app_filter] + + return [SchedulingEvent(code, self._event_name(code), start, finish) + for code, start, finish in slices] + + def _event_name(self, code): + if code in self.cpu_events: + return self.cpu_events[code] + return self.mpsoc_config.task_name_hash.get(code, "Undefined") + + # ------------------------------------------ + # Geometria + # ------------------------------------------ + def _name_width(self): + longest = max((len(name) for name in self.names.values()), default=0) + return max(longest * 10, 60) + + def _proportion(self): + if not self.last_time: + return 0.0 + return (self.width() - self._name_width()) / self.last_time + + def _time_scale(self): + """(ticks por unidade, nome da unidade) para a régua de tempo.""" + period = self.mpsoc_config.clock_period_in_ns or 10 + units = ((1_000_000, "ms"), (1_000, "us"), (period, "ns")) + for unit_ns, name in units: + ticks = max(1, round(unit_ns / period)) + if self.last_time >= ticks or name == "ns": + return ticks, name + return 1, "ns" + + # ------------------------------------------ + # Desenho + # ------------------------------------------ + def resizeEvent(self, event): + self._cache = None + super().resizeEvent(event) + + def changeEvent(self, event): + if event.type() == QEvent.Type.PaletteChange: + self._cache = None # troca de tema + super().changeEvent(event) + + def paintEvent(self, event): + painter = QPainter(self) + if self._cache is None: + self._cache = self._render_static() + painter.drawPixmap(0, 0, self._cache) + + text_color = self.palette().text().color() + if self.events: + self._draw_mouse_line(painter) + self._draw_report(painter, text_color) + else: + painter.setPen(text_color) + painter.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, "No scheduling events for this PE") + + def _render_static(self): + """Nomes, trechos e régua (só muda com tamanho, zoom ou tema).""" + ratio = self.devicePixelRatioF() + pixmap = QPixmap(self.size() * ratio) + pixmap.setDevicePixelRatio(ratio) + pixmap.fill(self.palette().base().color()) + if not self.events: + return pixmap + + painter = QPainter(pixmap) + width = self.width() + name_width = self._name_width() + proportion = self._proportion() + text_color = self.palette().text().color() + grid_color = QColor(Qt.GlobalColor.gray) + + painter.setPen(text_color) + if self.app_filter == -1: + painter.setFont(QFont("Arial", 13, QFont.Weight.Bold)) + title = f"PE {_router_label(self.mpsoc_config, self.router_address)} Scheduling Graph" + painter.drawText(QRect(0, 0, width, 30), Qt.AlignmentFlag.AlignCenter, title) + painter.setFont(QFont("Arial", 9)) + + for code, y in self.rows.items(): + painter.drawText(10, self.TOP + y + 5, self.names[code]) + + # Linhas horizontais de cada instância + painter.setPen(grid_color) + for y in self.rows.values(): + top = self.TOP + y - self.ROW_HEIGHT // 2 + painter.drawLine(name_width, top, width, top) + + # Trechos: verde para eventos da CPU, azul para tarefas + painter.setPen(Qt.PenStyle.NoPen) + for event in self.events: + x = int(event.start * proportion) + name_width + y = self.TOP + self.rows[event.code] - self.ROW_HEIGHT // 2 + slice_width = max(1, int((event.finish - event.start) * proportion)) + event.rect = QRect(x, y, slice_width, self.ROW_HEIGHT) + painter.fillRect(event.rect, self.CPU_COLOR if event.code in self.cpu_events else self.TASK_COLOR) + + # Eixos + painter.setPen(QPen(text_color, 2)) + painter.drawLine(name_width, self.TOP, name_width, self.time_line + 10) + painter.drawLine(name_width, self.time_line, width, self.time_line) + + # Régua: uma marca por unidade, pulando marcas que ficariam coladas + painter.setPen(text_color) + tick_scale, scale_name = self._time_scale() + step_px = tick_scale * proportion + skip = next(step for step in _RULER_STEPS if step * step_px >= 45 or step == _RULER_STEPS[-1]) + mark = skip + while mark * step_px <= width - name_width: + x = int(mark * step_px) + name_width + painter.drawLine(x, self.time_line, x, self.time_line + 10) + painter.drawText(QRect(x - 50, self.time_line + 10, 100, 16), + Qt.AlignmentFlag.AlignHCenter, f"{mark} {scale_name}") + mark += skip + + painter.end() + return pixmap + + def _draw_mouse_line(self, painter): + if self.mouse_x is None: + return + proportion = self._proportion() + ticks = int((self.mouse_x - self._name_width()) / proportion) if proportion else 0 + painter.setPen(QColor(Qt.GlobalColor.red)) + painter.drawLine(self.mouse_x, self.TOP, self.mouse_x, self.time_line) + label = f"{ticks} ticks" + x = self.mouse_x - 6 * len(label) if self.mouse_x >= self.width() - 100 else self.mouse_x - 2 * len(label) + painter.drawText(x, self.TOP - 10, label) + + def _draw_report(self, painter, text_color): + painter.setPen(text_color) + painter.setFont(QFont("Arial", 9)) + top = self.report_top + value_x = 200 + + if self.app_filter == -1: + painter.drawText(10, top, "----------------------- CPU utilization ------------------------") + painter.drawText(10, top + 15, "Total CPU time: ") + painter.drawText(value_x, top + 15, f"{self.total_cpu_time} ticks") + for line, code in enumerate(self.rows, start=2): + percent = self.usage[code] * 100.0 / self.total_cpu_time if self.total_cpu_time else 0.0 + painter.drawText(10, top + 15 * line, f"{self.names[code]} : ") + painter.drawText(value_x, top + 15 * line, f"{percent:.3f}%") + + if self.selected is None: + return + event = self.selected + x = 400 if self.app_filter == -1 else 10 + if self.mouse_x is not None and self.mouse_x > x: + x = self.mouse_x - 300 if self.mouse_x >= self.width() - 300 else self.mouse_x + lines = (("Name : ", event.name), + ("Start time : ", f"{event.start} ticks"), + ("Finish time : ", f"{event.finish} ticks"), + ("Slice Time : ", f"{event.finish - event.start} ticks")) + painter.drawText(x, top, "----------------------- Execution Slice------------------------") + for line, (label, value) in enumerate(lines, start=1): + painter.drawText(x, top + 15 * line, label) + painter.drawText(x + 200, top + 15 * line, value) + + # ------------------------------------------ + # Mouse + # ------------------------------------------ + def _event_at(self, point): + proportion = self._proportion() + if not proportion: + return None + # Trechos com início até a posição do mouse (com 1 px de tolerância) + tick = (point.x() + 1 - self._name_width()) / proportion + index = bisect.bisect_right(self.starts, tick) + for event in reversed(self.events[max(0, index - 64):index]): + if event.rect is not None and event.rect.adjusted(0, 0, 1, 1).contains(point): + return event + return None + + def mouseMoveEvent(self, event): + point = event.position().toPoint() + hit = self._event_at(point) + self.setCursor(Qt.CursorShape.PointingHandCursor if hit else Qt.CursorShape.ArrowCursor) + inside = point.x() >= self._name_width() and self.TOP <= point.y() <= self.time_line + self.mouse_x = point.x() if inside else None + self.update() + + def leaveEvent(self, event): + self.mouse_x = None + self.update() + super().leaveEvent(event) + + def mouseReleaseEvent(self, event): + if event.button() == Qt.MouseButton.LeftButton: + self.selected = self._event_at(event.position().toPoint()) + self.update() + + def wheelEvent(self, event): + if not event.modifiers() & Qt.KeyboardModifier.ControlModifier: + event.ignore() # rolagem normal do QScrollArea + return + factor = 1.1 if event.angleDelta().y() > 0 else 1 / 1.1 + self.setMinimumWidth(max(400, int(self.width() * factor))) + self.resize(self.minimumWidth(), self.height()) + event.accept() + + +class SchedulingGraphWindow(QWidget): + def __init__(self, mpsoc_config, router_address, app_filter=-1): + super().__init__() + self.graph = SchedulingGraph(mpsoc_config, router_address, app_filter) + + if app_filter == -1: + self.setWindowTitle(f"Scheduling Graph {_router_label(mpsoc_config, router_address)}") + else: + self.setWindowTitle(f"Application {app_filter} Scheduling Graph") + self.setWindowIcon(QPixmap(os.path.join(ICON_DIR, "scheduling_icon.png"))) + + scroll = QScrollArea(self) + scroll.setWidgetResizable(True) + scroll.setWidget(self.graph) + scroll.horizontalScrollBar().setSingleStep(200) + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(scroll) + + screen = self.screen().availableGeometry() + self.resize(screen.width(), min(screen.height(), self.graph.minimumHeight() + 30)) + self.move(screen.topLeft()) + + +# ========================================== +# TASK INFORMATION +# ========================================== +class TaskInfoWindow(QWidget): + """ + Estado de uma tarefa do PE e as mensagens que ela pediu (TaskInfoFrame.java). + Cada MESSAGE_REQUEST fica "pending" até chegar o MESSAGE_DELIVERY da + mesma tarefa remota, quando passa a "checked". + """ + + def __init__(self, mpsoc_config, router_window, task_id): + super().__init__() + self.mpsoc_config = mpsoc_config + self.router_window = router_window + self.task_id = task_id + name = _task_name(mpsoc_config, task_id) + self.setWindowTitle(f"Task {name} Information") + self.resize(560, 420) + + bold = QFont() + bold.setBold(True) + + def caption(text): + label = QLabel(text) + label.setFont(bold) + return label + + self.name_label = QLabel(name) + self.id_label = QLabel(str(task_id)) + self.status_label = QLabel() + self.time_label = QLabel() + + info = QGridLayout() + info.addWidget(caption("Task Name:"), 0, 0) + info.addWidget(self.name_label, 0, 1) + info.addWidget(caption("Task ID:"), 1, 0) + info.addWidget(self.id_label, 1, 1) + info.addWidget(caption("Task Status:"), 0, 2) + info.addWidget(self.status_label, 0, 3) + info.addWidget(caption("Executed Time:"), 1, 2) + info.addWidget(self.time_label, 1, 3) + info.setColumnStretch(1, 1) + info.setColumnStretch(3, 1) + + pipe_button = QPushButton("View PIPE") + request_button = QPushButton("Requested") + pipe_button.clicked.connect(lambda: self._show_comm_table("pipe", "Stored PIPE Messages")) + request_button.clicked.connect(lambda: self._show_comm_table("request", "Stored Message Requests")) + buttons = QHBoxLayout() + buttons.addWidget(pipe_button) + buttons.addWidget(request_button) + buttons.addStretch() + + self.message_table = _read_only_table(["Time: Requested / Delivered", "Remote task", "Delivered", "Num"]) + header = self.message_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + for column in (1, 2, 3): + header.setSectionResizeMode(column, QHeaderView.ResizeMode.ResizeToContents) + + layout = QVBoxLayout(self) + layout.addLayout(info) + layout.addLayout(buttons) + layout.addWidget(caption("Sent Message Requests:")) + layout.addWidget(self.message_table) + + self.comm_windows = [] + self.timer = QTimer(self) + self.timer.timeout.connect(self.refresh) + self.timer.start(TASK_INFO_REFRESH_MS) + self.refresh() + + def refresh(self): + try: + tasks = self.router_window.router_info.get_tasks_information() + except RuntimeError: + self.timer.stop() # janela do roteador já destruída + return + self._update_labels(tasks) + self._update_message_table(tasks) + + def _update_labels(self, tasks): + allocated, terminated = _task_lifetime(tasks, self.task_id) + if terminated is None: + self.status_label.setText("Running") + self.time_label.setText("-") + else: + self.status_label.setText("Terminated") + self.time_label.setText(str(terminated - allocated)) + + def _update_message_table(self, tasks): + pending = [] + rows = [] + for task in tasks: + if task.id != self.task_id: + continue + if task.service == "MESSAGE_REQUEST": + pending.append(task) + elif task.service == "MESSAGE_DELIVERY": + # A entrega responde o pedido mais antigo para a mesma tarefa remota + for index, request in enumerate(pending): + if request.remote_task_id == task.remote_task_id: + del pending[index] + rows.append((f"Requested: {request.time} Delivered: {task.time}", + task.remote_task_id, "checked")) + break + rows.extend((f"Requested: {request.time} Delivered: -", request.remote_task_id, "pending") + for request in pending) + + table = self.message_table + table.setRowCount(len(rows)) + counter = {} + for row, (times, remote, status) in enumerate(rows): + counter[remote] = counter.get(remote, 0) + 1 + values = (times, _task_name(self.mpsoc_config, remote), status, str(counter[remote])) + for column, value in enumerate(values): + item = QTableWidgetItem(value) + item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + if column == 2: + _highlight(item, CHECKED_BACKGROUND if status == "checked" else PENDING_BACKGROUND) + table.setItem(row, column, item) + + def _comm_file(self, folder): + """debug//.txt; os arquivos usam o endereço hamiltoniano.""" + config = self.mpsoc_config + address = self.router_window.router_address + candidates = [address] + if config.router_addressing == MPSoCConfig.XY: + candidates.append(_pe_xy_address(config, address)) # nome usado pelo Java + for candidate in candidates: + path = os.path.join(config.debug_file_path, folder, f"{candidate}.txt") + if os.path.isfile(path): + return path + return None + + def _show_comm_table(self, folder, title): + path = self._comm_file(folder) + rows = read_comm_debug(path) if path else None + if not rows: + what = "PIPE" if folder == "pipe" else "request" + QMessageBox.information(self, title, f"No {what} information") + return + + window = QWidget() + window.setWindowTitle(title) + window.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) + window.resize(320, 300) + table = _read_only_table(["Prod Task", "Cons Task", "Time"]) + table.setRowCount(len(rows)) + for row, (producer, consumer, time) in enumerate(rows): + values = (_task_name(self.mpsoc_config, producer), _task_name(self.mpsoc_config, consumer), time) + for column, value in enumerate(values): + table.setItem(row, column, QTableWidgetItem(value)) + layout = QVBoxLayout(window) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(table) + + window.destroyed.connect(lambda *_, w=window: self.comm_windows.remove(w)) + self.comm_windows.append(window) + window.move(self.geometry().center() - window.rect().center()) + window.show() + + def closeEvent(self, event): + self.timer.stop() + for window in list(self.comm_windows): + window.close() + super().closeEvent(event) + + +def read_comm_debug(path): + """ + Lê debug/pipe/.txt ou debug/request/.txt (add|remprodconstempo) + e retorna as mensagens que continuam armazenadas: [(prod, cons, tempo), ...]. + """ + stored = [] + try: + with open(path, "r", encoding="utf-8") as comm_file: + for line in comm_file: + fields = line.strip().split("\t") + if len(fields) < 4: + continue + try: + producer, consumer = int(fields[1]), int(fields[2]) + except ValueError: + continue + if fields[0] == "add": + stored.append((producer, consumer, fields[3])) + else: + # Remove a primeira mensagem com o mesmo produtor e consumidor + for index, (prod, cons, _time) in enumerate(stored): + if (prod, cons) == (producer, consumer): + del stored[index] + break + except OSError: + return None + return stored diff --git a/simulation.py b/simulation.py index b0a4d1c..6a3fc41 100644 --- a/simulation.py +++ b/simulation.py @@ -94,6 +94,10 @@ def __init__(self, mpsoc_config, mpsoc_information, router_matrix, parent=None): # chegaram; enquanto houver algum, as setas pintadas não são apagadas self.unfinished_packets = [] + # "Print Router Total Link Usage": roteador (-1 = desligado) e o que imprimir + self.link_usage_router = -1 + self.print_link_usage = True + self.checkpoint = CheckpointController(mpsoc_config.clock_period_in_ns, self.checkpoint_reached) self.timer = QTimer(self) @@ -275,8 +279,17 @@ def update_router_throughput(self, address, router_info, max_throughput): ports += [MPSoCConfig.NORTH0, MPSoCConfig.NORTH1] ports += [MPSoCConfig.LOCAL0, MPSoCConfig.LOCAL1] + total_link_usage = 0.0 for port in ports: - router.update_throughput(port, router_info.get_port_bandwidth_throughput_in_cycles(port) * percent) + usage = router_info.get_port_bandwidth_throughput_in_cycles(port) * percent + router.update_throughput(port, usage) + if port not in (MPSoCConfig.LOCAL0, MPSoCConfig.LOCAL1): + total_link_usage += usage + + if address == self.link_usage_router: + # Mesma saída do Java: um valor por checkpoint, com vírgula decimal + value = total_link_usage if self.print_link_usage else self.checkpoint.current_time_ms + print(f"{value:.6g}".replace(".", ","), flush=True) router_info.reset_bandwidth_throughput() router_info.reset_throughput() From 9ae2e9a5eeddefe6dca4898c9cbe2d6b8dce2d18 Mon Sep 17 00:00:00 2001 From: DrakRosmann Date: Fri, 25 Sep 2026 18:08:29 -0300 Subject: [PATCH 4/4] =?UTF-8?q?Malha=20de=20roteadores=20desenhada=20em=20?= =?UTF-8?q?vetor=20com=20zoom,=20Deloream=20com=20abas=20e=20novas=20janel?= =?UTF-8?q?as=20de=20an=C3=A1lise.=20[feature]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Matriz de roteadores reescrita com QGraphicsView: roteadores, setas e porta local desenhados pelo Qt (sem PNGs), zoom (Ctrl+roda, pinça, botões, Ctrl +/-/0, Fit) e periféricos em caixas fora do roteador - Exportação PNG da malha inteira, independente do zoom - Deloream: abas fecháveis por tarefa/aplicação, abertura com um clique, contagem de mensagens, busca na árvore e filtro das mensagens - Janelas de estatísticas de tráfego, timeline das aplicações, análise e visualização de caminhos - Leitura dos periféricos do YAML do testcase e exportação CSV/PNG - Suíte de testes (pytest) e documentação da apresentação Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01726MvBZ5qmM1pvD6QURd1T --- .github/workflows/build.yml | 7 + analysis.py | 667 +++++++++++ deloream.py | 417 +++++-- .../apresentacao_memphis_debugger.pptx | Bin 0 -> 806798 bytes documentation/implementacoes.md | 28 +- documentation/roteiro_apresentacao.md | 84 ++ export.py | 95 ++ filter_window.py | 20 + information/read_traffic_data.py | 14 +- information/router_information.py | 5 +- main.py | 137 ++- overview_windows.py | 40 +- path_view.py | 35 + pyproject.toml | 9 + router_info_window.py | 106 +- router_matrix.py | 1030 +++++++++++------ simulation.py | 98 +- statistics_windows.py | 828 +++++++++++++ testcase_files.py | 100 ++ tests/conftest.py | 100 ++ tests/test_analysis.py | 287 +++++ tests/test_config.py | 45 + tests/test_deloream.py | 50 + tests/test_export.py | 31 + tests/test_gui.py | 219 ++++ tests/test_information.py | 90 ++ tests/test_testcase_files.py | 44 + theme.py | 16 +- util/MPSoCConfig.py | 16 + uv.lock | 69 ++ 30 files changed, 4212 insertions(+), 475 deletions(-) create mode 100644 analysis.py create mode 100644 documentation/apresentacao_memphis_debugger.pptx create mode 100644 documentation/roteiro_apresentacao.md create mode 100644 export.py create mode 100644 path_view.py create mode 100644 statistics_windows.py create mode 100644 testcase_files.py create mode 100644 tests/conftest.py create mode 100644 tests/test_analysis.py create mode 100644 tests/test_config.py create mode 100644 tests/test_deloream.py create mode 100644 tests/test_export.py create mode 100644 tests/test_gui.py create mode 100644 tests/test_information.py create mode 100644 tests/test_testcase_files.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index de8273c..0c74e2d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,6 +22,13 @@ jobs: - name: Instalar dependências run: uv sync --locked + # Bibliotecas de sistema que o Qt precisa mesmo sem tela (plataforma offscreen) + - name: Instalar bibliotecas do Qt + run: sudo apt-get update && sudo apt-get install -y libegl1 libgl1 libxkbcommon0 libfontconfig1 libdbus-1-3 + + - name: Rodar testes + run: uv run --locked pytest -q + - name: Gerar executável run: | uv run --locked --with pyinstaller pyinstaller \ diff --git a/analysis.py b/analysis.py new file mode 100644 index 0000000..69da3d9 --- /dev/null +++ b/analysis.py @@ -0,0 +1,667 @@ +""" +Análises sobre o tráfego e os logs, inspiradas nas métricas do artigo do +Memphis (Ruaro et al., DAES 2019, Seções 3.4, 5 e 6): + + - aplicação de cada pacote (filtro por aplicação) + - latência de cada mensagem: da injeção na NoC até a entrega no destino + - vazão em Mbps e taxa de injeção (% da capacidade do enlace) + - linha do tempo da admissão de cada aplicação + +Não depende de Qt, para poder ser testado sem interface. +""" + +import os +import re +from collections import Counter, defaultdict +from dataclasses import dataclass, field + +from util.MPSoCConfig import MPSoCConfig + +# ID usado para "tarefa" do kernel/injetor nos campos de tarefa do traffic_router.txt +KERNEL_TASK_ID = 0xFFFF + +LOCAL_PORTS = (MPSoCConfig.LOCAL0, MPSoCConfig.LOCAL1) + +# Lado de onde o pacote chega em cada porta de entrada: (dx, dy) do vizinho +_PORT_SIDE = { + MPSoCConfig.EAST0: (1, 0), MPSoCConfig.EAST1: (1, 0), + MPSoCConfig.WEST0: (-1, 0), MPSoCConfig.WEST1: (-1, 0), + MPSoCConfig.NORTH0: (0, 1), MPSoCConfig.NORTH1: (0, 1), + MPSoCConfig.SOUTH0: (0, -1), MPSoCConfig.SOUTH1: (0, -1), +} + + +# ========================================== +# APLICAÇÕES +# ========================================== +def known_app_ids(mpsoc_config): + """IDs de aplicação conhecidos (platform.cfg); vazio = aceitar qualquer um.""" + ids = set(mpsoc_config.app_name_hash) + ids.update(task_id >> 8 for task_id in mpsoc_config.task_name_hash) + return ids + + +def app_label(mpsoc_config, app_id): + name = mpsoc_config.app_name_hash.get(app_id) + return f"{app_id} - {name}" if name else f"App {app_id}" + + +def task_app(task_id, known_apps=None): + """Aplicação dona da tarefa, ou None para campo vazio/kernel/ID desconhecido.""" + if task_id is None or task_id < 0 or task_id >= KERNEL_TASK_ID: + return None + app = task_id >> 8 + if known_apps and app not in known_apps: + return None + return app + + +def packet_apps(packet, known_apps=None): + """Aplicações envolvidas no pacote (pela tarefa de origem e/ou de destino).""" + apps = {task_app(packet.task_source, known_apps), task_app(packet.task_target, known_apps)} + apps.discard(None) + return apps + + +# ========================================== +# GEOMETRIA +# ========================================== +def xy(mpsoc_config, address): + xy_address = mpsoc_config.ham_to_xy_addr(address) + return xy_address >> 8, xy_address & 0xFF + + +def hops(mpsoc_config, source, target): + (sx, sy), (tx, ty) = xy(mpsoc_config, source), xy(mpsoc_config, target) + return abs(sx - tx) + abs(sy - ty) + + +def is_peripheral_input(mpsoc_config, router, port): + """Porta de borda sem roteador vizinho: o pacote veio de um periférico.""" + side = _PORT_SIDE.get(port) + if side is None: + return False + x, y = xy(mpsoc_config, router) + nx, ny = x + side[0], y + side[1] + return not (0 <= nx < mpsoc_config.mpsoc_x and 0 <= ny < mpsoc_config.mpsoc_y) + + +def _came_from(mpsoc_config, source, target, port): + """Com roteamento XY, o pacote de source chega em target por esta porta?""" + side = _PORT_SIDE.get(port) + if side is None: + return source == target + (sx, sy), (tx, ty) = xy(mpsoc_config, source), xy(mpsoc_config, target) + if sy == ty: # mesma linha: chega pelo lado de X + return side[1] == 0 and (sx - tx) * side[0] > 0 + return side[0] == 0 and (sy - ty) * side[1] > 0 + + +def median(values): + """Mediana (robusta às poucas latências distorcidas por registros faltando no trace).""" + ordered = sorted(values) + if not ordered: + return 0 + middle = len(ordered) // 2 + return ordered[middle] if len(ordered) % 2 else (ordered[middle - 1] + ordered[middle]) / 2 + + +def link_capacity_mbps(mpsoc_config): + """Um flit por ciclo: flit_size bits a cada clock_period ns.""" + if not mpsoc_config.clock_period_in_ns: + return 0.0 + return mpsoc_config.flit_size * 1000.0 / mpsoc_config.clock_period_in_ns + + +def mbps(mpsoc_config, flits, cycles): + """Vazão média de `flits` transmitidos em `cycles` ciclos.""" + if cycles <= 0 or not mpsoc_config.clock_period_in_ns: + return 0.0 + return flits * mpsoc_config.flit_size * 1000.0 / (cycles * mpsoc_config.clock_period_in_ns) + + +def neighbor(mpsoc_config, address, port): + """Roteador vizinho do lado da porta de entrada (None na borda ou porta local).""" + side = _PORT_SIDE.get(port) + if side is None: + return None + x, y = xy(mpsoc_config, address) + nx, ny = x + side[0], y + side[1] + if 0 <= nx < mpsoc_config.mpsoc_x and 0 <= ny < mpsoc_config.mpsoc_y: + return mpsoc_config.xy_to_ham_addr((nx << 8) | ny) + return None + + +def next_hop(mpsoc_config, address, target): + """Próximo roteador pelo roteamento XY (primeiro X, depois Y); None se já chegou.""" + (x, y), (tx, ty) = xy(mpsoc_config, address), xy(mpsoc_config, target) + if x != tx: + x += 1 if tx > x else -1 + elif y != ty: + y += 1 if ty > y else -1 + else: + return None + return mpsoc_config.xy_to_ham_addr((x << 8) | y) + + +def xy_consistent(mpsoc_config, address, port, target): + """ + Um pacote que entrou em `address` pela porta `port` indo para `target` + está de acordo com o roteamento XY? (Andando em X, o destino tem que + estar à frente em X; andando em Y, já tem que estar na coluna certa.) + """ + side = _PORT_SIDE.get(port) + if side is None: + return True + (x, y), (tx, ty) = xy(mpsoc_config, address), xy(mpsoc_config, target) + dx, dy = -side[0], -side[1] # sentido do movimento + if dy == 0: + return (tx - x) * dx >= 0 + return tx == x and (ty - y) * dy >= 0 + + +# ========================================== +# RASTREAMENTO DOS PACOTES (LATÊNCIA E ROTEAMENTO) +# ========================================== +@dataclass +class Message: + source: int # roteador de origem + target: int + service: int + flits: int # payload + header + injected: int # tick em que o header entrou na NoC + delivered: int = -1 # tick em que o último flit saiu do roteador de destino + task_source: int = -1 + task_target: int = -1 + from_peripheral: bool = False + apps: frozenset = frozenset() + hops: int = 0 + # Último roteador em que o pacote foi visto (para achar pacotes parados) + last_router: int = -1 + last_time: int = -1 + prev_router: int = -1 # roteador anterior ao último (-1 = ainda na origem) + hops_seen: int = 0 + # Registros do caminho: (roteador, porta de entrada, tick), a partir da injeção + path: list = field(default_factory=list) + + @property + def latency(self): + return self.delivered - self.injected if self.delivered >= 0 else -1 + + +@dataclass +class RoutingViolation: + """Registro em que o pacote entrou por uma porta incompatível com o XY.""" + time: int + router: int + port: int + target: int + service: int + task_source: int = -1 + task_target: int = -1 + message: "Message" = None # pacote ao qual o registro pertence, se identificado + + +class TrafficTracker: + """ + Acompanha cada pacote do traffic_router.txt pela NoC. + + - Injeção: entrada pela porta local ou por um periférico na borda. + - Cada registro seguinte (um por roteador do caminho) é associado ao + pacote em trânsito com o mesmo serviço e tamanho visto mais + recentemente no roteador vizinho do lado da porta de entrada. + - Chegada: registro em que roteador == destino. Dá a latência. + - Todo registro intermediário é conferido contra o roteamento XY. + + O traffic_router.txt não tem ID de pacote, e os campos de tarefa podem + mudar entre os hops de um mesmo pacote; por isso o casamento usa só + serviço, tamanho, o roteador vizinho e o tempo. Quando o trace omite + registros, a chegada é casada com a injeção mais recente cuja origem + seja compatível com a porta de entrada; injeções mais antigas do mesmo + fluxo que ficaram para trás são contadas como sem entrega registrada. + """ + + MAX_VIOLATIONS = 5000 # guardadas para a tabela (as contagens não têm limite) + + def __init__(self, mpsoc_config): + self.mpsoc_config = mpsoc_config + self.known_apps = known_app_ids(mpsoc_config) + self.reset() + + def reset(self): + self.messages = [] # na ordem de injeção + self._pending = defaultdict(list) + self.lost = 0 # injeções sem chegada registrada + self.consumed = 0 # pacotes do traffic_router.txt já analisados + self.hop_records = Counter() # serviço -> registros intermediários conferidos + self.hop_violations = Counter() # serviço -> registros fora do XY + self.violations = [] + + @staticmethod + def _key(packet): + # Sem as tarefas: no Memphis-V os campos de tarefa podem mudar de um + # hop para outro do mesmo pacote (valem os da injeção) + return packet.service, packet.size + + def consume(self, packet): + self.consumed += 1 + config = self.mpsoc_config + router = packet.router_address + + injected_locally = packet.input_port in LOCAL_PORTS + from_peripheral = not injected_locally and is_peripheral_input(config, router, packet.input_port) + + if not 0 <= packet.target_router < config.get_pe_number(): + # Destino fora da malha: não é um pacote na NoC (ex.: TASK_TERMINATED + # de tamanho 0 e destino -1 registrado pelo kernel na porta local) + return + + if injected_locally or from_peripheral: + message = Message(router, packet.target_router, packet.service, packet.size + 1, + packet.time, task_source=packet.task_source, task_target=packet.task_target, + from_peripheral=from_peripheral, + apps=frozenset(packet_apps(packet, self.known_apps)), + last_router=router, last_time=packet.time, + path=[(router, packet.input_port, packet.time)]) + self.messages.append(message) + if router == packet.target_router: + # Destino é o próprio roteador: entra e sai no mesmo registro + message.delivered = packet.time + packet.bandwidth_cycles + else: + self._pending[self._key(packet)].append(message) + return + + violation = self._check_routing(packet) + + candidates = self._pending.get(self._key(packet)) + if not candidates: + return + + # Pacote em trânsito visto por último no vizinho de onde este veio + previous = neighbor(config, router, packet.input_port) + match = None + for message in candidates: + if message.last_router == previous and message.last_time <= packet.time: + if match is None or message.last_time > match.last_time: + match = message + if match is not None: + match.prev_router = match.last_router + match.last_router = router + match.last_time = packet.time + match.hops_seen += 1 + match.path.append((router, packet.input_port, packet.time)) + if violation is not None: + violation.message = match + + if router != packet.target_router: + return + + if match is None: + # Faltaram registros no caminho: casa pela origem compatível com a porta + for message in reversed(candidates): + if message.injected <= packet.time and _came_from(config, message.source, router, + packet.input_port): + match = message + break + if match is None: + return + + # Mensagens do mesmo fluxo chegam em ordem: as anteriores não foram registradas + stale = [message for message in candidates + if message.source == match.source and message.injected < match.injected] + for message in stale: + candidates.remove(message) + self.lost += len(stale) + candidates.remove(match) + + match.target = router + match.hops = hops(config, match.source, router) + match.last_router, match.last_time = router, packet.time + match.delivered = packet.time + packet.bandwidth_cycles + + def _check_routing(self, packet): + """Confere o registro contra o XY; retorna a violação guardada, se houver.""" + self.hop_records[packet.service] += 1 + if xy_consistent(self.mpsoc_config, packet.router_address, packet.input_port, packet.target_router): + return None + self.hop_violations[packet.service] += 1 + if len(self.violations) >= self.MAX_VIOLATIONS: + return None + violation = RoutingViolation(packet.time, packet.router_address, packet.input_port, + packet.target_router, packet.service, packet.task_source, packet.task_target) + self.violations.append(violation) + return violation + + def delivered(self): + return [message for message in self.messages if message.delivered >= 0] + + def in_flight(self): + """Injetadas que ainda podem chegar (sem contar as dadas como perdidas).""" + return [message for queue in self._pending.values() for message in queue] + + def pending_count(self): + return sum(len(queue) for queue in self._pending.values()) + + +def read_all_packets(mpsoc_config): + """Todos os pacotes do traffic_router.txt, com um leitor próprio (não mexe na simulação).""" + from information.read_traffic_data import ReadTrafficData + from information.router_neighbors import RouterNeighbors + + reader = ReadTrafficData(mpsoc_config, RouterNeighbors.from_config(mpsoc_config)) + packets = [] + try: + while (packet := reader.get_next_packet()) is not None: + packets.append(packet) + finally: + reader.close() + return packets + + +def track_all(mpsoc_config, packets): + tracker = TrafficTracker(mpsoc_config) + for packet in packets: + tracker.consume(packet) + return tracker + + +# Um pacote que acabou de chegar gera a interrupção logo depois: os pacotes +# entregues um pouco antes do início do trecho também são listados +SLICE_LOOKBACK = 100 + + +def messages_in_slice(messages, pe, start, finish, lookback=SLICE_LOOKBACK): + """ + Pacotes de um PE durante um trecho do escalonamento: [(mensagem, "received" | "sent")], + em ordem de tempo. Recebidos: entregues ao PE entre start - lookback e finish. + Enviados: injetados pelo PE entre start e finish. + """ + result = [] + for message in messages: + if message.target == pe and message.delivered >= 0 and start - lookback <= message.delivered <= finish: + result.append((message.delivered, message, "received")) + if message.source == pe and not message.from_peripheral and start <= message.injected <= finish: + result.append((message.injected, message, "sent")) + result.sort(key=lambda item: item[0]) + return [(message, direction) for _time, message, direction in result] + + +# ========================================== +# VALIDAÇÃO DO ROTEAMENTO E DEADLOCK +# ========================================== +# Um serviço com pelo menos esta fração de registros fora do XY não é uma +# falha pontual: é tráfego que não usa XY (ex.: inundação da rede de +# broadcast de gerência, no canal LOW do Memphis-V) +NON_XY_RATIO = 0.3 +NON_XY_MIN_RECORDS = 3 + + +def routing_summary(tracker): + """ + Retorna (serviços não-XY {serviço: (fora do XY, total)}, violações + pontuais dos demais serviços). + """ + non_xy = {service: (count, tracker.hop_records[service]) + for service, count in tracker.hop_violations.items() + if count >= NON_XY_MIN_RECORDS and count / tracker.hop_records[service] >= NON_XY_RATIO} + anomalies = [violation for violation in tracker.violations if violation.service not in non_xy] + return non_xy, anomalies + + +def blocked_messages(tracker, now, threshold): + """Pacotes em trânsito sem avançar há pelo menos `threshold` ciclos, do mais antigo ao mais novo.""" + stuck = [message for message in tracker.in_flight() if now - message.last_time >= threshold] + stuck.sort(key=lambda message: message.last_time) + return stuck + + +def _held_link(message): + """Enlace ocupado pelo header: (roteador anterior, roteador atual).""" + return (message.prev_router if message.hops_seen else "PE", message.last_router) + + +def _wanted_link(mpsoc_config, message): + """Próximo enlace que o pacote precisa, pelo XY.""" + following = next_hop(mpsoc_config, message.last_router, message.target) + return (message.last_router, following if following is not None else "PE") + + +def find_wait_cycles(mpsoc_config, messages): + """ + Espera circular entre pacotes parados: A espera o enlace que B ocupa, + B espera o de C, ..., até voltar a A. Com roteamento XY isso não deveria + acontecer; um ciclo indica deadlock. Retorna listas de mensagens. + """ + holders = defaultdict(list) + for message in messages: + holders[_held_link(message)].append(message) + + waits = {id(message): holders.get(_wanted_link(mpsoc_config, message), []) for message in messages} + by_id = {id(message): message for message in messages} + + cycles, seen = [], set() + for start in messages: + path, index = [], {} + current = start + while current is not None and id(current) not in seen: + if id(current) in index: + cycle = path[index[id(current)]:] + cycles.append(cycle) + break + index[id(current)] = len(path) + path.append(current) + blockers = waits.get(id(current), []) + current = by_id.get(id(blockers[0])) if blockers else None + seen.update(id(message) for message in path) + return cycles + + +@dataclass +class AppTraffic: + app: int + messages: int = 0 + flits: int = 0 + first_injection: int = -1 + last_delivery: int = -1 + latency_sum: int = 0 + latency_max: int = 0 + hops_sum: int = 0 + latencies: list = field(default_factory=list) + + @property + def active_cycles(self): + return self.last_delivery - self.first_injection if self.messages else 0 + + @property + def average_latency(self): + return self.latency_sum / self.messages if self.messages else 0.0 + + @property + def median_latency(self): + return median(self.latencies) + + @property + def average_hops(self): + return self.hops_sum / self.messages if self.messages else 0.0 + + +def traffic_by_app(messages): + """Totais por aplicação sobre as mensagens entregues.""" + stats = {} + for message in messages: + if message.delivered < 0: + continue + for app in message.apps: + entry = stats.setdefault(app, AppTraffic(app)) + entry.messages += 1 + entry.flits += message.flits + if entry.first_injection < 0 or message.injected < entry.first_injection: + entry.first_injection = message.injected + entry.last_delivery = max(entry.last_delivery, message.delivered) + entry.latency_sum += message.latency + entry.latency_max = max(entry.latency_max, message.latency) + entry.hops_sum += message.hops + entry.latencies.append(message.latency) + return stats + + +# ========================================== +# LINHA DO TEMPO DA ADMISSÃO DE APLICAÇÕES +# ========================================== +@dataclass +class AppTimeline: + """ + Etapas do protocolo de admissão (Seção 3.4 do artigo) de uma aplicação. + Tempos em ticks; -1 = etapa não encontrada. + """ + app: int + name: str = "" + task_count: int = 0 + received: int = -1 # mapper recebeu o pedido (NEW APP) + requester: str = "" + mapped: int = -1 # mapeamento concluído + started: int = -1 # TASK_RELEASE enviado: aplicação liberada para executar + terminated: int = -1 + task_allocated: dict = field(default_factory=dict) # tarefa -> (tick, PE) da chegada do código + task_terminated: dict = field(default_factory=dict) # tarefa -> tick + task_confirmed: set = field(default_factory=set) # TASK_ALLOCATED recebidos pelo mapper + + @property + def first_allocation(self): + return min((time for time, _pe in self.task_allocated.values()), default=-1) + + @property + def last_allocation(self): + return max((time for time, _pe in self.task_allocated.values()), default=-1) + + @property + def last_termination(self): + return max(self.task_terminated.values(), default=-1) + + @property + def end(self): + return self.terminated if self.terminated >= 0 else self.last_termination + + @property + def start(self): + return self.started if self.started >= 0 else self.last_allocation + + @property + def admission_cycles(self): + """Do pedido até a liberação (o artigo cita ~40 mil ciclos para o MPEG).""" + begin = self.received if self.received >= 0 else self.first_allocation + return self.start - begin if begin >= 0 and self.start >= 0 else -1 + + @property + def mapping_cycles(self): + return self.mapped - self.received if self.received >= 0 and self.mapped >= 0 else -1 + + @property + def execution_cycles(self): + return self.end - self.start if self.start >= 0 and self.end >= 0 else -1 + + @property + def first_event(self): + times = [t for t in (self.received, self.mapped, self.started, self.first_allocation) if t >= 0] + return min(times, default=-1) + + +# Mensagens do mapper_task no log do processador (prefixo $$$____) +_NEW_APP = re.compile(r"New app received at (\d+) from (\w+) with (\d+) tasks") +_MAPPED = re.compile(r"Mapped with score .*? at (\d+)") +_TASK_ALLOCATED = re.compile(r"Received task allocated from id (\d+)") +_RELEASE = re.compile(r"Sending TASK_RELEASE for app (\d+)") +_STARTED = re.compile(r"App (\d+) started at (\d+)") +_TERMINATED = re.compile(r"App (\d+) terminated at time (\d+)") + + +def _log_dir(mpsoc_config): + testcase = mpsoc_config.get_testcase_path() or os.path.dirname(os.path.normpath(mpsoc_config.debug_file_path)) + return os.path.join(testcase, "log") + + +def read_mapper_log(mpsoc_config, timelines): + """ + Completa as linhas do tempo com o log do mapper_task. Retorna o caminho + do log usado, ou None se nenhum log tiver mensagens do mapper. + """ + log_dir = _log_dir(mpsoc_config) + try: + names = sorted(name for name in os.listdir(log_dir) if name.endswith(".txt")) + except OSError: + return None + + for name in names: + path = os.path.join(log_dir, name) + try: + with open(path, "r", encoding="utf-8", errors="replace") as log_file: + lines = [line for line in log_file if "New app received" in line or "App " in line + or "Mapped with" in line or "task allocated" in line or "TASK_RELEASE for app" in line] + except OSError: + continue + if not any(_NEW_APP.search(line) for line in lines): + continue + + # O pedido ainda não tem ID de aplicação: ele só aparece no TASK_RELEASE + pending = None + for line in lines: + if match := _NEW_APP.search(line): + pending = {"received": int(match[1]), "requester": match[2], "tasks": int(match[3]), + "mapped": -1, "confirmed": set()} + elif (match := _MAPPED.search(line)) and pending is not None: + pending["mapped"] = int(match[1]) + elif (match := _TASK_ALLOCATED.search(line)) and pending is not None: + pending["confirmed"].add(int(match[1])) + elif match := _RELEASE.search(line): + timeline = timelines.setdefault(int(match[1]), AppTimeline(int(match[1]))) + if pending is not None: + timeline.received = pending["received"] + timeline.requester = pending["requester"] + timeline.task_count = pending["tasks"] + timeline.mapped = pending["mapped"] + timeline.task_confirmed = pending["confirmed"] + pending = None + elif match := _STARTED.search(line): + timelines.setdefault(int(match[1]), AppTimeline(int(match[1]))).started = int(match[2]) + elif match := _TERMINATED.search(line): + timelines.setdefault(int(match[1]), AppTimeline(int(match[1]))).terminated = int(match[2]) + return path + return None + + +def build_timelines(mpsoc_config, packets): + """ + Linha do tempo de cada aplicação a partir de todos os pacotes do + traffic_router.txt (chegada do código das tarefas e término) e do log + do mapper_task (pedido, mapeamento, liberação e término da aplicação). + + Retorna (timelines ordenadas por ID, caminho do log do mapper ou None). + """ + known_apps = known_app_ids(mpsoc_config) + timelines = {} + + for packet in packets: + if packet.router_address != packet.target_router: + continue + # Tarefa chegando ao PE de destino (mesma regra do RouterInformation) + if packet.service in mpsoc_config.task_allocation_services: + app = task_app(packet.task_source, known_apps) + if app is not None: + timeline = timelines.setdefault(app, AppTimeline(app)) + timeline.task_allocated.setdefault( + packet.task_source, (packet.time + packet.bandwidth_cycles, packet.router_address)) + + for packet in packets: + if packet.input_port in LOCAL_PORTS and packet.service in mpsoc_config.task_terminated_services: + task = packet.task_source if packet.task_source >= 0 else packet.task_target + app = task_app(task, known_apps) + if app is not None: + timelines.setdefault(app, AppTimeline(app)).task_terminated.setdefault(task, packet.time) + + log_path = read_mapper_log(mpsoc_config, timelines) + + for app, timeline in timelines.items(): + timeline.name = mpsoc_config.app_name_hash.get(app, "") + if not timeline.task_count: + timeline.task_count = len(timeline.task_allocated) + return [timelines[app] for app in sorted(timelines)], log_path diff --git a/deloream.py b/deloream.py index 4e2df8c..11d7ce8 100644 --- a/deloream.py +++ b/deloream.py @@ -20,12 +20,15 @@ import html import os import re +from collections import defaultdict from dataclasses import dataclass -from PySide6.QtCore import Qt -from PySide6.QtGui import QAction, QFont, QIcon, QKeySequence -from PySide6.QtWidgets import (QFileDialog, QMainWindow, QMessageBox, QSplitter, QTextBrowser, - QTreeWidget, QTreeWidgetItem) +from PySide6.QtCore import QSortFilterProxyModel, Qt +from PySide6.QtGui import QAction, QFont, QIcon, QKeySequence, QStandardItem, QStandardItemModel +from PySide6.QtWidgets import (QAbstractItemView, QFileDialog, QHBoxLayout, QHeaderView, QLabel, QLineEdit, + QMainWindow, QMenu, QMessageBox, QSplitter, QStackedWidget, QTableView, + QTabWidget, QTreeWidget, QTreeWidgetItem, QTreeWidgetItemIterator, + QVBoxLayout, QWidget) ICON_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icon", "delorean.png") @@ -35,8 +38,9 @@ # $$$____; a mensagem pode conter "_" _MESSAGE_PATTERN = re.compile(r"^\$\$\$_(?P[^_]+)_(?P\d+)_(?P\d+)_(?P.*)$") -# Papel dos itens da árvore que guarda o ID da aplicação/tarefa -_ID_ROLE = Qt.ItemDataRole.UserRole +# Papéis dos itens da árvore: tipo ("app"/"task") e ID da aplicação/tarefa +_KIND_ROLE = Qt.ItemDataRole.UserRole +_ID_ROLE = Qt.ItemDataRole.UserRole + 1 @dataclass(frozen=True) @@ -92,12 +96,12 @@ def read_name_relations(platform_path): return tasks, apps -def read_task_messages(log_dir, app_id, task_id): +def read_all_messages(log_dir): """ - Mensagens de uma tarefa em todos os logs do cenário, na ordem dos - arquivos (ordenados por nome) e das linhas. + Todas as mensagens "$$$" dos logs do cenário, agrupadas por + (aplicação, tarefa local), na ordem dos arquivos (por nome) e das linhas. """ - messages = [] + messages = defaultdict(list) for file_name in sorted(os.listdir(log_dir)): if "log" not in file_name: continue @@ -110,39 +114,169 @@ def read_task_messages(log_dir, app_id, task_id): if not line.startswith(MESSAGE_MARK): continue message = TaskMessage.parse(line) - if message is not None and message.app_id == app_id and message.task_id == task_id: - messages.append(message) + if message is not None: + messages[(message.app_id, message.task_id)].append(message) + + return dict(messages) - return messages + +def read_task_messages(log_dir, app_id, task_id): + """Mensagens de uma tarefa em todos os logs do cenário.""" + return read_all_messages(log_dir).get((app_id, task_id), []) + + +class MessagesTab(QWidget): + """ + Aba com as mensagens de uma tarefa (ou de todas as tarefas de uma + aplicação): cabeçalho com o resumo, campo de busca e tabela + #, processador, [tarefa,] mensagem. + """ + + def __init__(self, kind, item_id, parent=None): + super().__init__(parent) + self.kind = kind + self.item_id = item_id + + self.header = QLabel() + self.header.setTextFormat(Qt.TextFormat.RichText) + self.header.setWordWrap(True) + + self.search = QLineEdit() + self.search.setPlaceholderText("Filter messages…") + self.search.setClearButtonEnabled(True) + self.search.setMaximumWidth(260) + + self.model = QStandardItemModel(0, 4, self) + self.model.setHorizontalHeaderLabels(["#", "Processor", "Task", "Message"]) + self.proxy = QSortFilterProxyModel(self) + self.proxy.setSourceModel(self.model) + self.proxy.setFilterKeyColumn(-1) + self.proxy.setFilterCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) + self.search.textChanged.connect(self._filter) + + self.table = QTableView() + self.table.setModel(self.proxy) + self.table.setAlternatingRowColors(True) + self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + self.table.setWordWrap(False) + self.table.verticalHeader().hide() + self.table.verticalHeader().setDefaultSectionSize(22) + header = self.table.horizontalHeader() + header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + header.setStretchLastSection(True) + header.setDefaultAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter) + font = QFont("DejaVu Sans Mono") + font.setStyleHint(QFont.StyleHint.Monospace) + self.table.setFont(font) + # Coluna "Task" só faz sentido na aba de uma aplicação + self.table.setColumnHidden(2, kind == "task") + + self.empty = QLabel() + self.empty.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.empty.setWordWrap(True) + self.stack = QStackedWidget() + self.stack.addWidget(self.table) + self.stack.addWidget(self.empty) + + top = QHBoxLayout() + top.addWidget(self.header, 1) + top.addWidget(self.search) + + layout = QVBoxLayout(self) + layout.setContentsMargins(8, 8, 8, 8) + layout.addLayout(top) + layout.addWidget(self.stack, 1) + + def set_messages(self, title, subtitle, messages, empty_text): + """`messages` é uma lista de (nome da tarefa, TaskMessage).""" + self.model.removeRows(0, self.model.rowCount()) + for number, (task_name, message) in enumerate(messages, start=1): + index_item = QStandardItem() + index_item.setData(number, Qt.ItemDataRole.DisplayRole) + row = [index_item, QStandardItem(message.proc), QStandardItem(task_name), + QStandardItem(message.message)] + for item in row: + item.setToolTip(message.message) + self.model.appendRow(row) + + procs = list(dict.fromkeys(message.proc for _task, message in messages)) + details = [html.escape(subtitle), f"{len(messages)} message{'s' if len(messages) != 1 else ''}"] + if procs: + label = "processor" if len(procs) == 1 else "processors" + details.append(f"{label}: {html.escape(', '.join(procs))}") + self.header.setText(f"{html.escape(title)}" + f"
{' · '.join(details)}") + + self.empty.setText(empty_text) + self.stack.setCurrentWidget(self.table if messages else self.empty) + self.search.setEnabled(bool(messages)) + + def _filter(self, text): + self.proxy.setFilterFixedString(text) class DeloreamWindow(QMainWindow): - """Árvore Aplicações -> Tarefas e, ao ativar uma tarefa, as mensagens dela nos logs.""" + """ + Árvore Aplicações -> Tarefas à esquerda; um clique numa tarefa (ou + aplicação) abre as mensagens dela numa aba à direita, ou volta para a aba + já aberta. + """ def __init__(self, testcase_path, parent=None): super().__init__(parent) self.setWindowTitle("Deloream - Debug Log Reader for MPSoCs") if os.path.isfile(ICON_PATH): self.setWindowIcon(QIcon(ICON_PATH)) - self.resize(800, 700) + self.resize(1000, 700) + + self.task_names, self.app_names, self.messages = {}, {}, {} + self._error = None + + self.tree_filter = QLineEdit() + self.tree_filter.setPlaceholderText("Search tasks…") + self.tree_filter.setClearButtonEnabled(True) + self.tree_filter.textChanged.connect(self._filter_tree) self.tree = QTreeWidget() - self.tree.setHeaderHidden(True) - # Duplo clique ou Enter, como o duplo clique do original + self.tree.setHeaderLabels(["Task", "Messages"]) + self.tree.setRootIsDecorated(True) + self.tree.setUniformRowHeights(True) + self.tree.header().setStretchLastSection(False) + self.tree.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + self.tree.header().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) + # Um clique abre a aba; Enter também (navegar com as setas não abre) + self.tree.itemClicked.connect(self._on_item_activated) self.tree.itemActivated.connect(self._on_item_activated) - self.text = QTextBrowser() - self.text.setOpenLinks(False) - font = QFont("DejaVu Sans Mono") - font.setStyleHint(QFont.StyleHint.Monospace) - font.setPointSize(11) - self.text.setFont(font) + left = QWidget() + left_layout = QVBoxLayout(left) + left_layout.setContentsMargins(6, 6, 0, 6) + left_layout.addWidget(self.tree_filter) + left_layout.addWidget(self.tree) + + self.tabs = QTabWidget() + self.tabs.setTabsClosable(True) + self.tabs.setMovable(True) + self.tabs.setDocumentMode(True) + self.tabs.tabCloseRequested.connect(self.close_tab) + self.tabs.currentChanged.connect(self._sync_tree_selection) + self.tabs.tabBar().setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.tabs.tabBar().customContextMenuRequested.connect(self._tab_menu) + self.tabs.tabBar().installEventFilter(self) + + self.placeholder = QLabel() + self.placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.placeholder.setWordWrap(True) + self.right = QStackedWidget() + self.right.addWidget(self.placeholder) + self.right.addWidget(self.tabs) splitter = QSplitter() - splitter.addWidget(self.tree) - splitter.addWidget(self.text) + splitter.addWidget(left) + splitter.addWidget(self.right) splitter.setStretchFactor(1, 1) - splitter.setSizes([250, 550]) + splitter.setSizes([280, 720]) self.setCentralWidget(splitter) self._build_menu() @@ -161,6 +295,18 @@ def _build_menu(self): reload_action.triggered.connect(lambda: self.load_testcase(self.testcase_path)) options.addAction(reload_action) + options.addSeparator() + close_tab = QAction("Close tab", self) + close_tab.setShortcut(QKeySequence("Ctrl+W")) + close_tab.triggered.connect(lambda: self.close_tab(self.tabs.currentIndex())) + options.addAction(close_tab) + + close_all = QAction("Close all tabs", self) + close_all.setShortcut(QKeySequence("Ctrl+Shift+W")) + close_all.triggered.connect(self.close_all_tabs) + options.addAction(close_all) + + options.addSeparator() exit_action = QAction("Exit", self) exit_action.setShortcut(QKeySequence("Ctrl+E")) exit_action.triggered.connect(self.close) @@ -171,8 +317,9 @@ def _build_menu(self): about.triggered.connect(lambda: QMessageBox.about( self, "Deloream", "Deloream - Debug Log Reader for MPSoCs\n\n" - "Double-click a task to read its debug messages ($$$) " - "from the processor logs of the testcase.")) + "Click a task to read its debug messages ($$$) from the processor logs " + "of the testcase in a new tab; click an application to see the messages " + "of all its tasks.")) help_menu.addAction(about) # ========================================== @@ -190,71 +337,205 @@ def _choose_testcase(self): path = QFileDialog.getExistingDirectory(self, "Open testcase (folder with debug/ and log/)", self.testcase_path) if path: + if path != self.testcase_path: + self.close_all_tabs() self.load_testcase(path) def load_testcase(self, testcase_path): - """Lê os nomes do platform.cfg e monta a árvore de aplicações.""" + """ + Lê os nomes do platform.cfg e as mensagens dos logs, monta a árvore + de aplicações e atualiza as abas abertas. + """ self.testcase_path = testcase_path self.setWindowTitle(f"Deloream - {testcase_path}") self.tree.clear() - self.text.clear() + self._error = None try: self.task_names, self.app_names = read_name_relations(self.platform_path) except OSError: - self.task_names, self.app_names = {}, {} - self._show_error(f"platform.cfg not found in {html.escape(self.platform_path)}") + self.task_names, self.app_names, self.messages = {}, {}, {} + self._error = f"platform.cfg not found in {self.platform_path}" + self.close_all_tabs() + self._update_placeholder() return - root = QTreeWidgetItem(self.tree, ["Applications"]) + try: + self.messages = read_all_messages(self.log_dir) + except OSError: + self.messages = {} # Aplicações do BEGIN_app_name_relation; tarefas sem aplicação conhecida # também aparecem, com o ID da aplicação no lugar do nome app_ids = sorted(set(self.app_names) | {task_id >> 8 for task_id in self.task_names}) for app_id in app_ids: - app_name = self.app_names.get(app_id, "app") - app_item = QTreeWidgetItem(root, [f"{app_name}[{app_id}]"]) + app_item = QTreeWidgetItem(self.tree, [self._app_title(app_id)]) + app_item.setData(0, _KIND_ROLE, "app") app_item.setData(0, _ID_ROLE, app_id) + bold = app_item.font(0) + bold.setBold(True) + app_item.setFont(0, bold) + app_total = 0 for task_id in sorted(self.task_names): - if task_id >> 8 == app_id: - task_item = QTreeWidgetItem(app_item, [f"{self.task_names[task_id]}[{task_id}]"]) - task_item.setData(0, _ID_ROLE, task_id) + if task_id >> 8 != app_id: + continue + count = len(self._task_messages(task_id)) + app_total += count + task_item = QTreeWidgetItem(app_item, [self._task_title(task_id), str(count) if count else "—"]) + task_item.setData(0, _KIND_ROLE, "task") + task_item.setData(0, _ID_ROLE, task_id) + task_item.setTextAlignment(1, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) + if not count: + task_item.setForeground(0, self.palette().placeholderText()) + task_item.setForeground(1, self.palette().placeholderText()) + task_item.setToolTip(0, "No debug message found in the logs") + + app_item.setText(1, str(app_total) if app_total else "—") + app_item.setTextAlignment(1, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) + app_item.setToolTip(0, "Click to see the messages of all tasks of this application") self.tree.expandAll() + self._filter_tree(self.tree_filter.text()) + + # Abas abertas passam a mostrar os logs recarregados + for index in range(self.tabs.count()): + self._fill_tab(self.tabs.widget(index)) + self._update_placeholder() + + def _app_title(self, app_id): + return f"{self.app_names.get(app_id, 'app')}[{app_id}]" + + def _task_title(self, task_id): + return f"{self.task_names.get(task_id, 'task')}[{task_id}]" + + def _task_messages(self, task_id): + return self.messages.get((task_id >> 8, task_id & 0xFF), []) + + def _filter_tree(self, text): + """Mostra só as tarefas (e aplicações) cujo nome contém o texto.""" + text = text.strip().lower() + for app_index in range(self.tree.topLevelItemCount()): + app_item = self.tree.topLevelItem(app_index) + app_matches = text in app_item.text(0).lower() + any_task = False + for task_index in range(app_item.childCount()): + task_item = app_item.child(task_index) + visible = app_matches or text in task_item.text(0).lower() + task_item.setHidden(not visible) + any_task |= visible + app_item.setHidden(not (app_matches or any_task)) # ========================================== - # MENSAGENS + # ABAS # ========================================== - def _on_item_activated(self, item, _column): - # Só as folhas (tarefas) mostram mensagens - if item.parent() is None or item.parent().parent() is None: - return - self.show_task_messages(item.data(0, _ID_ROLE)) + def _on_item_activated(self, item, _column=0): + kind = item.data(0, _KIND_ROLE) + if kind is not None: + self.open_tab(kind, item.data(0, _ID_ROLE)) def show_task_messages(self, task_id): - app_id, local_task_id = task_id >> 8, task_id & 0xFF - - try: - messages = read_task_messages(self.log_dir, app_id, local_task_id) - except OSError: - messages = [] - - if not messages: - self._show_error("Log file not created yet or no message found!") + """Abre (ou mostra) a aba com as mensagens da tarefa.""" + return self.open_tab("task", task_id) + + def find_tab(self, kind, item_id): + for index in range(self.tabs.count()): + tab = self.tabs.widget(index) + if tab.kind == kind and tab.item_id == item_id: + return index + return -1 + + def open_tab(self, kind, item_id): + index = self.find_tab(kind, item_id) + if index < 0: + tab = MessagesTab(kind, item_id) + self._fill_tab(tab) + index = self.tabs.addTab(tab, "") + self._update_tab_title(index) + self.tabs.setCurrentIndex(index) + self._update_placeholder() + return self.tabs.widget(index) + + def _fill_tab(self, tab): + if tab.kind == "task": + task_id = tab.item_id + title = self._task_title(task_id) + subtitle = f"application {self._app_title(task_id >> 8)}" + name = self.task_names.get(task_id, str(task_id)) + messages = [(name, message) for message in self._task_messages(task_id)] + empty = "Log file not created yet or no message found for this task." + else: + app_id = tab.item_id + title = self._app_title(app_id) + task_ids = [task_id for task_id in sorted(self.task_names) if task_id >> 8 == app_id] + subtitle = f"{len(task_ids)} task{'s' if len(task_ids) != 1 else ''}" + messages = [(self.task_names[task_id], message) + for task_id in task_ids for message in self._task_messages(task_id)] + empty = "Log file not created yet or no message found for this application." + tab.set_messages(title, subtitle, messages, empty) + + def _update_tab_title(self, index): + tab = self.tabs.widget(index) + if tab.kind == "task": + title = self._task_title(tab.item_id) + tooltip = f"{title} - application {self._app_title(tab.item_id >> 8)}" + else: + title = self._app_title(tab.item_id) + tooltip = f"All tasks of {title}" + self.tabs.setTabText(index, title) + self.tabs.setTabToolTip(index, tooltip) + + def close_tab(self, index): + if index < 0: return - - name = html.escape(self.task_names.get(task_id, str(task_id))) - parts = [f"

{name}[{task_id}]

"] - current_proc = None - for message in messages: - # Cabeçalho a cada troca de processador (a tarefa pode ter migrado) - if message.proc != current_proc: - current_proc = message.proc - parts.append(f"

-- Processor: {html.escape(current_proc)}

") - parts.append(f"

{html.escape(message.message)}

") - - self.text.setHtml("".join(parts)) - - def _show_error(self, text): - self.text.setHtml(f"{text}") + tab = self.tabs.widget(index) + self.tabs.removeTab(index) + tab.deleteLater() + self._update_placeholder() + + def close_all_tabs(self, keep=None): + for index in reversed(range(self.tabs.count())): + if self.tabs.widget(index) is not keep: + self.close_tab(index) + + def _tab_menu(self, position): + index = self.tabs.tabBar().tabAt(position) + if index < 0: + return + tab = self.tabs.widget(index) + menu = QMenu(self) + menu.addAction("Close", lambda: self.close_tab(self.tabs.indexOf(tab))) + menu.addAction("Close other tabs", lambda: self.close_all_tabs(keep=tab)) + menu.addAction("Close all tabs", self.close_all_tabs) + menu.exec(self.tabs.tabBar().mapToGlobal(position)) + + def eventFilter(self, obj, event): + # Botão do meio fecha a aba, como nos navegadores + if obj is self.tabs.tabBar() and event.type() == event.Type.MouseButtonRelease \ + and event.button() == Qt.MouseButton.MiddleButton: + self.close_tab(obj.tabAt(event.position().toPoint())) + return True + return super().eventFilter(obj, event) + + def _sync_tree_selection(self, index): + """Seleciona na árvore o item da aba atual.""" + if index < 0: + return + tab = self.tabs.widget(index) + iterator = QTreeWidgetItemIterator(self.tree) + while iterator.value(): + item = iterator.value() + if item.data(0, _KIND_ROLE) == tab.kind and item.data(0, _ID_ROLE) == tab.item_id: + self.tree.setCurrentItem(item) + return + iterator += 1 + + def _update_placeholder(self): + if self._error: + self.placeholder.setText(f"{html.escape(self._error)}") + elif not self.task_names: + self.placeholder.setText("No task found in platform.cfg.") + else: + self.placeholder.setText("Click a task to read its debug messages" + "
Click an application to see the messages of all its tasks.") + self.right.setCurrentWidget(self.tabs if self.tabs.count() and not self._error else self.placeholder) diff --git a/documentation/apresentacao_memphis_debugger.pptx b/documentation/apresentacao_memphis_debugger.pptx new file mode 100644 index 0000000000000000000000000000000000000000..24319e9249a0f0ce44c8786dfe59d93d40e6793a GIT binary patch literal 806798 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

=;Q8Eizo0 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ⅈ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

n54<-_oNMXLq2NWU zl}jb~-~opbAB0MTH2d7?)bi~YV15D<0lL$^uL1`gHhuH`DlbeBn5S4$0%U{t2R5cx z0E1cwb`tLHovzQ!j@i$hdy;u0%0oXtJK4Rh;uLT+&UN_OP(Yy7KF2SX+z{AU?Y|6aO(W;?cZHn8(V?FH=m37 zRiB+@!gF9azNA<3*K|V&iYiPW*6Iv7YOR&P8ai|4aD2Synl*bxn_J%RL5Q8QqG@af z>j>fjokmS$AIPNgD$MP>aEkSF4mAprg+)Zif|DZu+YKsZrHsYol<8k({=I$1xp|w7 zF}4tfG|@`raO3i+!Z{9m zhKniPFyuhOSo>w-)T!ymkBcd~tzKQ|rh)gtt9S61KGOeV@~-2YIm4sP;-_zUp7D-~ zZ=b21wY3+lQfR=}3;h2rlfY>gB6|K}(1Gljg{jD#fJqf+?7*r9v%Sb`NEU(PP#pt_ zbIT2+E|+~qv-rK?Z2+jnl`rjBuJ;YS&)Aqdft~na_EYl@%rqc!k{uw@*~LNw)x7<4 z@!!$o$6I44>bzB^QuZ!z2Ve7mK${b-QW1M-hbC@{`XI@+D#YBzy0^m zjNWx}lK=iWw9EegBHok5cJFTA>G$u8fxRuR5mMS5CUmOvgAq$rXX%f)N6xX0@9XDc zf^64cN`kb1=+JIr!yFMwd^SmI=Jo~lt|d|uksh8OC6ZdWNqhQ#Z`fd@7L6O519Hiu z_iyKPbPu`9-!L`)cgOXo;#7gtm9?o_wExIJm( zqgT&=e=arahm|pjJ;I0NpQ{kbw$MzP>;H#R1o=`%1|kzWWSPtUd+*BO9v|GkfnCt` zXLb4&#bh|lpU?K5-S(ZeZmN>RrbYR^CI0rmJrNj@^v-P}oX@j|S&#E#yZ7$h!Ezw8 z%#S8Vr0oxd;Bl1~=YPzxV|xq0^-(s_Q)>0|Z#h;H7$`1F?Sk*|a;2zM$r=Ek%@hNdi~RZmE9>qh*#x^z$VRvaH` zdMGL=1e%>BArgi+r?pdO(xe2s1JI47ssA>hV~+^$&rQTs))iHB*B%3(&=R71rtR4R zKLT?vDp`~Q)RgK7qmZE^V!?_j7Vk!K=|mV+ib)ALcXP6>mN;K)ATds~Z^2`g*6S z=)U$_zu^w0YoDJycPh60oj}iN274jVTGe^$_vq?-5@M;U zBS+@eFk2_1zRruQ@l(q)mq`w{_+|3W@=I{bQv&1g-~;(aP1Aqz_$wy?;US|6?5aqh ztL#q>9$Pq8UA+WL$T4VDFt=NNxsaZ*Zu~fhzJmv+QzgUHXb0nc_$d#~M9x%0DLIVu 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

~=^SL;Q`L`F6c@IhW3UtVj2lwx< z#6^J!bMKcqF^=IrQuA-1!G4rM=D3rURXM_el0K0WMhqLqfaQ@t=fgi?-ff%MT8LH| z>#D4xDsj*RAw6`l!7(`(2)g&!?Y?n~-A(=wR2C^8!8u`M6A~UL9KTEjVwdJG(fRto zz%Y!J>{X5)#2!RXd7ie1$wFLm_^`w&^Q_0$#(YidVXn;K2eo83?Rp|BKm9T7&btpE zm^X(k?5SZL*#K+M>JSLq&kBBqyHVyjn}iFR16cELXN;XV;%avnp_p9E(PEO3zkb80 zcr;$ScHKXl(EF#|=f z`9odEdO3KoS6B|pcS?uZ)PgV=RBh%nQ#eVWCIZS|vQD@}>Nj;nWX-Kk{{_EHIe=Zt zlcl-89!!TH8)LF{3+;}p>(R8dF_*4{^to|_lASLKFy|0qRI%T;f2MA@nU(nbgZ*V2*i})c{yeJAPyx$@@X(Dh!ckV>YzA$b?dk@2r0P7Somw*G9D7Jk0!Kf(021v8n zGU8%-EAGzYP~h|024|vw5CCvJJwZ2rcxuKDcZ~_f6H15oY|U*~F|jJAH**nsoA$_E zn8HHa8y8#cs2I~Y2%wt~QnLQ;y9rUz10(N!S&TB#NP}4kLUE;OVx+B|EkypXA7tkH zN_Dm78T%iUl=eTeQ|fV6K>Muq%a@;Gjd?9O2B3n(;^C6vl;z~25C9a8)_1M6Io+av zykJJ_EKMfIw6J>=N?=rnERFyR;Jd@Jw193#D8H`%ClFRPcFg+zACyvN<~i$@&EGsa zwuP=u;}ucqV6iN-rNdRD?U)8Hu-svZC~l7FTHSNj_1!p9!miV-&dXD*`Vcq;o{r&}Tnc=zmNGZaR3O|N5W$xS< zuZRcpuTn9ZeJ6@q($5_k`Gdv}lZX+@AYOt#HYq8)q7L0h!ILLPch)+M)-4TFjJ9JQ z>EZ)skX~)DYIIC*XUk` z^-W10hL)5vf_lT~w|c{PLz>a`_0AuJ*ZA?jz=8{=-3{7cAq;84WBcany7o-c<4SK6 zLgayqT#D-cssB9yGlb3z$~3v8bq@-)Ys$=@CON%8MF|k%FycfR##B()Lau=2t5#_M zS`nTEokkan50V1`n&$ho#{4irXm(P|Ukq^befYR^P88a3fYB}e+VHZn$Gu5WJGeT% zs=$rU0V_4&wf^^EKq8vBP}e$D|1uFw6q_HI?q zoOSfF7Fapji*etJRT#TDdv^aVM!mvsFC_be!nk(RHH?yfUHWsFi@LBvRGVx zg}Z?`#=u`pC&P;1TfxkKsa-K#WCgN^l#EDm-FC>;lcQ})G~^y^M@><-_9}{KIG2f} zmG9c+gmV$FjfG3G%ktW6q!Dy;&HD9(Yy44}N2x-Vz#0QJl+YM|;f$1mT%4W17Pq#{ zFju7NW2c{cu>vP0MrL^UZ||@EZ58s8=26Hjh%5i2029TxD^3KP{C|Y>jGFcho)v%g zEW^k>0qHq)h75}OzY-%T_Z1W@z%HKj5q#my#bHCQMUT1>w2o#Nza&nF2MvrO?~gp6 zOED?QjcJQ-F0Mho50j1&OmMGqZV|&B6B|p-gP8DhNeQD`!VevqM-~#vVg8GJ9np4Z za`_;+x_<4NHqbg_efbHDtxN88!j|3-&rw!Yp+G_a`Niq|TD_Cur6ncKY~`=SkLlw= zk3L-g{~vce_a3N$>!rD=Njbz4%RGY^4;&EG8OnCAI4((^;Gh!Mfz*)B%eYT|Yswg= zqsC4uzPx%qBL2k0MNu`&Cmk~CsjQq?Q3r`Z=*>pO;rjZvOr$%0oZ%{14IIM{8LYWM zz7n0Clhal>JRy_eTMc*)_(uon8#J{1zZ77oMsdcyE?2AO2eBIC@`

E3hybBh1yK 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 literal 0 HcmV?d00001 diff --git a/documentation/implementacoes.md b/documentation/implementacoes.md index 90d4401..cb83114 100644 --- a/documentation/implementacoes.md +++ b/documentation/implementacoes.md @@ -49,6 +49,12 @@ diretório debug/ | `filter_window.py` | Janela Service and PE Filter (menu Filters, Ctrl+F): escolhe quais pacotes a simulação processa, por serviço (Only/Except) e por PE | `FilterForm.java` | | `projects.py` | Projetos salvos (`.hdf`): File → Save / Open / Delete Project | `MainFrame.java` (save/open/deleteMenuItem) | | `help_dialogs.py` | Help → About e Help → Packet Format | `util/AboutFrame.java`, `MainFrame.packetFormatMenuItemActionPerformed` | +| `analysis.py` | Análises sem interface: aplicação de cada pacote, latência (casamento injeção → entrega), vazão em Mbps e linha do tempo da admissão de aplicações | — (métricas do artigo do Memphis, DAES 2019) | +| `statistics_windows.py` | Janelas Traffic Statistics (Links, Applications, Messages, Routing, Blocked) e Application Timeline (menu Tools) | — | +| `path_view.py` | Liga as janelas de análise à malha principal: duplo clique num pacote pinta o caminho dele e destaca o roteador | — | +| `testcase_files.py` | Leitura dos periféricos do YAML do testcase (`/.yaml`, seção `hw → Peripherals`) | — | +| `export.py` | Exportação de tabelas em CSV e de janelas/gráficos em PNG (botões e menu do botão direito) | — | +| `tests/` | Testes automatizados (pytest), com o cenário extraído do `DEBUG EXAMPLE.zip` | — | | `deloream.py` | Deloream: leitor das mensagens `$$$` das tarefas nos logs dos processadores | `deloream/DeloreamMainFrame.java`, `deloream/TaskMessage.java` | | `theme.py` | Paletas dos temas claro/escuro, cores da grade e preferência salva | — | | `router_matrix.py` | Desenho da grade e de cada roteador (imagens, setas, %) | `Roteador.java`, `UJPanelImagem.java`, `MainFrame.createNoCPanel` | @@ -489,8 +495,17 @@ Classes/funções: `TaskMessage.parse(line)`, `read_name_relations(platform_path ## 12. Limitações e próximos passos - **Back To** só vai até tempos já simulados (mesmo comportamento do Java). -- **Filtro de serviços/PEs** (`Filters → Service and PE Filter`): ainda não implementado. O controlador já aceita `SimulationController.packet_filter`. -- O relatório de uso de link por roteador (`printRouterTotalLinkUsage`, oculto no Java) não foi portado. +- **Latência e caminho dos pacotes** (Tools → Traffic Statistics): o `traffic_router.txt` não tem ID de pacote e os campos de tarefa podem mudar entre os hops de um mesmo pacote. Cada pacote é seguido hop a hop por serviço, tamanho, roteador vizinho (pela porta de entrada) e tempo; as tarefas da injeção definem a aplicação. No exemplo, 1 388 de 1 388 pacotes são entregues e todos os que têm hops são seguidos pelo caminho inteiro. A latência é só a da rede (sem o tempo do kernel). +- **Validação do roteamento** (aba Routing): cada registro intermediário é conferido contra o XY (primeiro X, depois Y). Serviços com muitos registros fora do XY não são tratados como erro, e sim listados como não-XY: no Memphis-V são os serviços 0–3 do canal LOW, que se espalham pela malha inteira (inundação da rede de gerência). +- **Pacotes parados e deadlock** (aba Blocked): lista os pacotes em trânsito sem avançar há N ciclos e procura espera circular (cada pacote esperando o enlace ocupado pelo seguinte). Com XY não deveria haver ciclo. +- **Ordem do `traffic_router.txt`**: o arquivo não está estritamente em ordem de tempo (a linha de um pacote longo pode ser gravada depois de registros mais novos). O **Back To** usa busca binária pelo tempo e herda essa pequena imprecisão do Java. +- **Caminho do pacote na malha**: duplo clique nas abas Messages, Routing e Blocked (Traffic Statistics) ou na tabela de pacotes do gráfico de escalonamento pinta o caminho registrado na janela principal e destaca o roteador (destino, onde parou ou onde violou o XY). A simulação é parada para o desenho não ser apagado; `File → Reset Graphical Path` limpa. +- **Escalonamento × pacotes**: clicar num trecho do gráfico de escalonamento lista os pacotes que o PE recebeu (entregues até 100 ciclos antes do trecho ou durante ele) e enviou (injetados durante ele) — ex.: o pacote que causou uma interrupção. +- **Periféricos**: os nomes vêm do YAML do testcase, lido linha a linha (sem PyYAML). Sem o YAML, a borda é marcada como "Peripheral" quando passa o primeiro pacote vindo de fora da malha. +- **Follow Live Trace** (`File`): no fim do `traffic_router.txt` a simulação não para; tenta ler novas linhas a cada 250 ms. Uma linha sem `\n` no fim é considerada incompleta e só é lida depois de terminar de ser gravada. A Application Timeline e as análises do trace inteiro são atualizadas com **Reload**. +- **Registros sem destino na malha**: `TASK_TERMINATED` de tamanho 0 com destino -1 é um aviso do kernel na porta local, não um pacote; é ignorado nas estatísticas. +- **Application Timeline**: o pedido, o mapeamento, a liberação e o término das aplicações vêm das mensagens do `mapper_task` no log (`log/logx.txt`). Sem esse log, só aparecem a chegada do código e o término das tarefas. +- **Término de tarefa**: no Memphis-V o `TASK_TERMINATED` traz a tarefa no último campo (o Java lia o penúltimo, do formato do HeMPS, e as tarefas nunca apareciam como terminadas). Corrigido em `RouterInformation`. --- @@ -504,4 +519,13 @@ Classes/funções: `TaskMessage.parse(line)`, `read_name_relations(platform_path 6. `File → Reset Simulation` volta ao início. 7. Abra `Tools → Communication Overview`, `Tools → Task Mapping Overview` e `Tools → Message Log` e rode a simulação: as porcentagens, as tarefas e as mensagens mudam conforme os pacotes avançam (2 755 mensagens ao fim do exemplo). +### Testes automatizados + +``` +uv sync # instala também o pytest (grupo dev) +uv run pytest # ~4 s +``` + +Os testes rodam sem tela (Qt offscreen), extraem o cenário do `DEBUG EXAMPLE.zip` e usam QSettings e pasta de projetos temporárias. O workflow `.github/workflows/build.yml` roda os testes antes de gerar o executável. + Resultados esperados com o exemplo: 3 101 pacotes, último pacote em 6 251 176 ticks (62,51176 ms com clock de 10 ns), 42 serviços e 11 tarefas. diff --git a/documentation/roteiro_apresentacao.md b/documentation/roteiro_apresentacao.md new file mode 100644 index 0000000..11add1f --- /dev/null +++ b/documentation/roteiro_apresentacao.md @@ -0,0 +1,84 @@ +# Roteiro da apresentação — Memphis-V Graphical Debugger (v0.1.0-alpha) + +Slides: `documentation/apresentacao_memphis_debugger.pptx` (15 slides). O texto completo de cada fala está nas **notas do apresentador** de cada slide; este roteiro resume o tempo, os pontos-chave e os momentos de demonstração. + +**Duração total:** ~20 minutos + perguntas. + +--- + +## Antes de começar (checklist) + +- [ ] Extrair o `DEBUG EXAMPLE.zip` e anotar o caminho de `LOGS/example_testcase/example_scenario/debug/` +- [ ] Abrir o debugger (`./dist/MemphisVDebugger/MemphisVDebugger` ou `uv run main.py`) e deixar minimizado +- [ ] Deixar o tema **claro** (Edit → Dark Mode desmarcado), para a demonstração começar igual aos slides +- [ ] Testar o projetor/compartilhamento de tela com a janela do debugger (a grade de roteadores precisa estar legível) +- [ ] Ter a release `v0.1.0-alpha` aberta no navegador, para mostrar no final + +--- + +## Parte 1 — Contexto (≈ 3,5 min) + +| Slide | Tempo | Pontos-chave | +|---|---|---| +| 1. Capa | 1 min | Refatoração Java → Python/PySide6; é uma **alpha**: muito já funciona, mas não tudo. A imagem é o sistema novo em execução. | +| 2. Memphis-V e o Graphical Debugger | 1,5 min | Memphis-V simula MPSoCs com NoC; o debugger reproduz os arquivos gravados pela simulação. Cenário de exemplo: 9 PEs, 3.101 pacotes, 11 tarefas. | +| 3. Por que migrar | 1 min | Os três objetivos do README: interface moderna, manutenção simples, execução sem JVM. | + +## Parte 2 — Como o sistema funciona (≈ 7,5 min) + +| Slide | Tempo | Pontos-chave | +|---|---|---| +| 4. Arquitetura | 2 min | Arquivos → `MPSoCConfig` / `ReadTrafficData` → `MPSoCInformation` → controlador e janelas. Leitura **sob demanda** e cache para voltar no tempo. Nomes das classes iguais aos do Java. | +| 5. traffic_router.txt | 1,5 min | Ler a linha de exemplo campo a campo. Abertura validada: sem os 3 arquivos, nada é carregado. | +| 6. Janela principal | 2 min | Os 5 elementos numerados. **→ Demonstração 1** | +| 7. Como a simulação funciona | 2 min | Ciclo de 4 passos; checkpoint de 0,5 ms; velocidades 1–98 vs. 99–100; QTimer no lugar da thread do Java. | + +### 🎬 Demonstração 1 (no slide 6, ~2 min dentro do tempo do slide) + +1. **File → New Debugging** → selecionar a pasta `debug/` do exemplo. +2. Clicar **>||** umas 5 vezes: mostrar a tabela *Current Packet* e o tempo mudando. +3. Colocar a velocidade em ~90 e dar **>** (play): mostrar as setas vermelhas se movendo. +4. **STOP**, olhar o tempo em ticks no *Speed Control*, digitar em **Back To** um valor menor que ele (ex.: metade) e clicar **Go**. +5. *(Opcional)* Mostrar o erro ao abrir uma pasta sem os arquivos (ex.: `LOGS/example_testcase/debug/`, que não tem `traffic_router.txt`). + +## Parte 3 — Janelas de análise (≈ 5,5 min) + +| Slide | Tempo | Pontos-chave | +|---|---|---| +| 8. Communication Overview | 1,5 min | Mapa de calor azul → vermelho; filtros de serviço e volume/banda; *Statistics*. **Novo:** acompanha a simulação (o Java era uma foto). | +| 9. Task Mapping e Message Log | 1,5 min | RUN/TER por PE, uma cor por aplicação, botão para congelar; Message Log com `---->>>` (envio) e `<<<----` (recebimento), 2.755 linhas no fim. | +| 10. Deloream | 1,5 min | Lê `log/`; formato `$$$_proc_app_tarefa_mensagem`; duplo clique na tarefa. Correções: `_` preservado, Exit não fecha o debugger. | +| 11. Modo escuro e melhorias | 1 min | **→ Demonstração 2** | + +### 🎬 Demonstração 2 (no slide 11, ~1,5 min) + +1. Com a simulação parada no meio, abrir **Tools → Communication Overview** e **Tools → Task Mapping Overview** lado a lado. +2. Dar **>** na janela principal e mostrar as duas janelas se atualizando sozinhas. +3. **Tools → Deloream** → duplo clique em `prod[769]`. +4. **Ctrl+T** para alternar para o modo escuro (e de volta). + +## Parte 4 — Situação e próximos passos (≈ 4 min) + +| Slide | Tempo | Pontos-chave | +|---|---|---| +| 12. Bugs corrigidos | 1,5 min | Seis exemplos; a lista completa está em `documentation/implementacoes.md` (seção 11). | +| 13. Status | 1,5 min | **10 já refatorados × 6 faltando** (contagem por funcionalidade). Deixar claro: por isso é alpha. | +| 14. Próximos passos | 1 min | Estabilizar (CI Linux + Windows) → filtros e detalhes do roteador → projetos → beta. | + +## Encerramento (≈ 1 min + perguntas) + +| Slide | Tempo | Pontos-chave | +|---|---|---| +| 15. Como experimentar | 1 min | Executável da release, `uv run main.py`, primeiro teste com o `DEBUG EXAMPLE.zip`. Pedir relatos nas Issues. Abrir para perguntas. | + +--- + +## Perguntas prováveis + +| Pergunta | Resposta curta | +|---|---| +| Funciona no Windows? | O código não tem nada específico de Linux, mas o executável atual é só Linux. O build de Windows (PyInstaller em uma máquina Windows ou no CI) é o primeiro item do roteiro. | +| Por que o Back To não avança para o futuro? | Ele só volta para tempos já lidos do arquivo — mesmo comportamento do original. Para avançar, usa-se o play em velocidade 99–100. | +| Os resultados batem com o debugger Java? | A lógica foi portada classe a classe. As diferenças são as correções de bugs listadas no slide 12 (e na documentação). | +| Arquivos grandes de tráfego ficam lentos? | O arquivo é lido sob demanda e as janelas redesenham no máximo 2×/s; o modo rápido processa lotes de 200 pacotes. Ainda não foi testado com cenários muito maiores que o exemplo. | +| O que acontece com cenários de outro tamanho (4×4, 8×8)? | A grade, as janelas e os cálculos usam `mpsoc_x`/`mpsoc_y` do `platform.cfg`; clusters são desenhados com borda. | diff --git a/export.py b/export.py new file mode 100644 index 0000000..7d275c9 --- /dev/null +++ b/export.py @@ -0,0 +1,95 @@ +""" +Exportação de tabelas (CSV) e janelas/gráficos (PNG). + +Qualquer QTableWidget/QTableView exporta o que está visível (com filtros e +ordenação aplicados). install_export_actions() coloca "Export CSV…" e/ou +"Save as PNG…" no menu de contexto (botão direito) de um widget. +""" + +import csv +import os + +from PySide6.QtCore import QSettings, Qt +from PySide6.QtGui import QAction +from PySide6.QtWidgets import QFileDialog, QMessageBox + +_SETTINGS_KEY = "export/last_dir" + + +def _settings(): + return QSettings("GAPH", "MemphisGraphicalDebugger") + + +def _ask_path(parent, title, default_name, file_filter, extension): + directory = _settings().value(_SETTINGS_KEY, os.path.expanduser("~")) + path, _ = QFileDialog.getSaveFileName(parent, title, os.path.join(directory, default_name), file_filter) + if not path: + return None + if not path.lower().endswith(extension): + path += extension + _settings().setValue(_SETTINGS_KEY, os.path.dirname(path)) + return path + + +def model_rows(model): + """Cabeçalhos e linhas (texto exibido) de um modelo de tabela.""" + columns = range(model.columnCount()) + headers = [str(model.headerData(column, Qt.Orientation.Horizontal) or "") for column in columns] + rows = [[str(model.data(model.index(row, column)) or "") for column in columns] + for row in range(model.rowCount())] + return headers, rows + + +def write_csv(model, path): + headers, rows = model_rows(model) + with open(path, "w", newline="", encoding="utf-8") as csv_file: + writer = csv.writer(csv_file) + writer.writerow(headers) + writer.writerows(rows) + return len(rows) + + +def save_png(widget, path): + # Vistas com zoom/rolagem (ex.: a malha) exportam o conteúdo inteiro + image = widget.to_image() if hasattr(widget, "to_image") else widget.grab() + if not image.save(path, "PNG"): + raise OSError(f"could not write {path}") + + +def export_csv(parent, view, default_name): + """Pergunta o arquivo e salva a tabela `view` (QTableWidget ou QTableView).""" + path = _ask_path(parent, "Export CSV", default_name + ".csv", "CSV (*.csv)", ".csv") + if path is None: + return None + try: + write_csv(view.model(), path) + except OSError as error: + QMessageBox.critical(parent, "Export CSV", f"Could not save the file:\n{error}") + return None + return path + + +def export_png(parent, widget, default_name): + """Pergunta o arquivo e salva uma imagem do `widget` como ele aparece na tela.""" + path = _ask_path(parent, "Save as PNG", default_name + ".png", "PNG image (*.png)", ".png") + if path is None: + return None + try: + save_png(widget, path) + except OSError as error: + QMessageBox.critical(parent, "Save as PNG", f"Could not save the image:\n{error}") + return None + return path + + +def install_export_actions(widget, default_name, csv_view=None, png_widget=None): + """Menu de contexto com as exportações disponíveis para o widget.""" + widget.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu) + if csv_view is not None: + action = QAction("Export CSV…", widget) + action.triggered.connect(lambda: export_csv(widget.window(), csv_view, default_name)) + widget.addAction(action) + if png_widget is not None: + action = QAction("Save as PNG…", widget) + action.triggered.connect(lambda: export_png(widget.window(), png_widget, default_name)) + widget.addAction(action) diff --git a/filter_window.py b/filter_window.py index 067d140..787a868 100644 --- a/filter_window.py +++ b/filter_window.py @@ -6,6 +6,8 @@ - aba Services: só os serviços da lista ("Only") ou todos menos eles ("Except") - aba PE: só os pacotes capturados nos roteadores da lista + - aba Application: só os pacotes de/para tarefas das aplicações da lista + (não existe no Java; o artigo do Memphis cita o filtro por aplicação) A lista montada numa aba só passa a valer ao clicar em "Apply" (lista vazia desliga o filtro daquela aba). Pacotes rejeitados não atualizam os roteadores @@ -16,6 +18,7 @@ from PySide6.QtWidgets import (QComboBox, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QMessageBox, QPushButton, QRadioButton, QTabWidget, QVBoxLayout, QWidget) +import analysis from util.MPSoCConfig import MPSoCConfig # Serviço sempre descartado pelo filtro do Java @@ -109,7 +112,9 @@ def __init__(self, mpsoc_config, parent=None): self.filtered_services = set() self.filtered_pes = set() + self.filtered_apps = set() self.except_services = False + self.known_apps = analysis.known_app_ids(mpsoc_config) self.ignored_service = mpsoc_config.get_service_value(IGNORED_SERVICE) services = [(mpsoc_config.get_string_service_name(service), service) @@ -129,6 +134,10 @@ def __init__(self, mpsoc_config, parent=None): self.services_tab = _FilterTab("Services", "Added Services", services, self._apply_services, self.close, mode) self.pe_tab = _FilterTab("PEs", "Added PEs", pes, self._apply_pes, self.close) + apps = [(analysis.app_label(mpsoc_config, app), app) for app in sorted(self.known_apps)] + self.app_tab = _FilterTab("Applications", "Added Applications", apps, self._apply_apps, self.close) + self.app_tab.setToolTip("Keeps only packets whose source or target task belongs to the selected " + "applications.\nKernel packets without task IDs are hidden.") self.status_label = QLabel() self.status_label.setWordWrap(True) @@ -136,6 +145,7 @@ def __init__(self, mpsoc_config, parent=None): tabs = QTabWidget() tabs.addTab(self.services_tab, "Services") tabs.addTab(self.pe_tab, "PE") + tabs.addTab(self.app_tab, "Application") layout = QVBoxLayout(self) layout.addWidget(tabs) @@ -159,6 +169,10 @@ def _apply_pes(self, pes): self.filtered_pes = set(pes) self._update_status() + def _apply_apps(self, apps): + self.filtered_apps = set(apps) + self._update_status() + def _update_status(self): config = self.mpsoc_config parts = [] @@ -168,6 +182,9 @@ def _update_status(self): if self.filtered_pes: names = ", ".join(self._pe_label(address) for address in sorted(self.filtered_pes)) parts.append(f"Only PEs: {names}") + if self.filtered_apps: + names = ", ".join(analysis.app_label(config, app) for app in sorted(self.filtered_apps)) + parts.append(f"Only applications: {names}") self.status_label.setText("Active filter — " + "; ".join(parts) if parts else "No filter applied") # ------------------------------------------ @@ -187,4 +204,7 @@ def filter(self, packet): if self.filtered_pes and packet.router_address not in self.filtered_pes: return False + if self.filtered_apps and not analysis.packet_apps(packet, self.known_apps) & self.filtered_apps: + return False + return True diff --git a/information/read_traffic_data.py b/information/read_traffic_data.py index 04fdf66..00a8666 100644 --- a/information/read_traffic_data.py +++ b/information/read_traffic_data.py @@ -18,6 +18,9 @@ def __init__(self, mpsoc_config, neighbors): self.neighbors = neighbors self.packet_read_control = 0 self.all_packets = [] + # Arquivo ainda sendo gravado pelo simulador: uma linha sem "\n" no + # fim está incompleta e só é lida quando terminar de ser gravada + self.follow = False traffic_path = os.path.join(mpsoc_config.debug_file_path, "traffic_router.txt") # Gera FileNotFoundError caso o arquivo não exista @@ -39,13 +42,20 @@ def get_packet_counter_by_time(self, time): def get_next_packet(self): # Ainda não lido: busca a próxima linha válida do arquivo if self.packet_read_control == len(self.all_packets): - for line in self.traffic: + while True: + # tell() em arquivo texto é lento: só é preciso para desfazer uma linha incompleta + position = self.traffic.tell() if self.follow else None + line = self.traffic.readline() + if not line: + return None # fim do arquivo (por enquanto, no modo follow) + if self.follow and not line.endswith("\n"): + self.traffic.seek(position) + return None packet = self._parse_packet(line) if packet is not None: self.all_packets.append(packet) self.packet_read_control += 1 return packet - return None packet = self.all_packets[self.packet_read_control] self.packet_read_control += 1 diff --git a/information/router_information.py b/information/router_information.py index a8877ee..62f5675 100644 --- a/information/router_information.py +++ b/information/router_information.py @@ -37,7 +37,10 @@ def _update_task_information(self, packet): # Pacote está saindo do PE elif packet.input_port in (MPSoCConfig.LOCAL0, MPSoCConfig.LOCAL1): if packet.service in config.task_terminated_services: - self.tasks.append(TaskInformation(packet.task_source, "TERMINATED", packet.time)) + # No Memphis-V a tarefa terminada vem no último campo (task_source = -1); + # no formato do HeMPS, usado pelo Java, vinha no penúltimo + task_id = packet.task_source if packet.task_source >= 0 else packet.task_target + self.tasks.append(TaskInformation(task_id, "TERMINATED", packet.time)) elif packet.service == config.get_service_value("MESSAGE_REQUEST"): self.tasks.append(TaskInformation(packet.task_target, "MESSAGE_REQUEST", packet.time, packet.task_source)) diff --git a/main.py b/main.py index 24fce8b..bb48294 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,7 @@ from PySide6.QtGui import Qt, QPalette, QColor, QAction, QKeySequence from PySide6.QtWidgets import (QApplication, QMainWindow, QFileDialog, QMessageBox, QWidget, QInputDialog, - QVBoxLayout, QHBoxLayout, QScrollArea, QTableWidgetItem, QTableWidget, QLabel, + QVBoxLayout, QHBoxLayout, QTableWidgetItem, QTableWidget, QLabel, QHeaderView) import theme @@ -14,11 +14,15 @@ from simulation import SimulationController, MAX_REPAINT_SPEED, ticks_to_ms from overview_windows import CommunicationOverviewWindow, MessageLogWindow, TaskMappingWindow from deloream import DeloreamWindow +from statistics_windows import ApplicationTimelineWindow, TrafficStatisticsWindow from router_info_window import RouterInfoWindow from platform_setup import PlatformSetupDialog from filter_window import FilterWindow from help_dialogs import AboutDialog, show_packet_format import projects +import export +import path_view +import testcase_files class MinhaJanela(QMainWindow, MainWindow): @@ -26,7 +30,6 @@ class MinhaJanela(QMainWindow, MainWindow): mpconfig = None mpsoc_information = None router_matrix_widget = None - frame_scroll_area = None simulation = None communication_window = None task_mapping_window = None @@ -35,6 +38,8 @@ class MinhaJanela(QMainWindow, MainWindow): router_info_windows = None simulation_ticks = 0 filter_window = None + traffic_statistics_window = None + timeline_window = None WINDOW_TITLE = "Memphis-V Graphical Debugger" @@ -72,6 +77,17 @@ def __init__(self): self.actionMessage_Log.setEnabled(False) self.actionTask_List.setEnabled(False) + # Ferramentas baseadas nas métricas do artigo do Memphis (não existem no Java) + self.actionTraffic_Statistics = QAction("Traffic Statistics", self) + self.actionApplication_Timeline = QAction("Application Timeline", self) + self.menuTools.insertAction(self.actionDeloream, self.actionTraffic_Statistics) + self.menuTools.insertAction(self.actionDeloream, self.actionApplication_Timeline) + self.menuTools.insertSeparator(self.actionDeloream) + for action in (self.actionTraffic_Statistics, self.actionApplication_Timeline): + action.setEnabled(False) + self.actionTraffic_Statistics.triggered.connect(self.open_traffic_statistics) + self.actionApplication_Timeline.triggered.connect(self.open_application_timeline) + # Atalhos do menu File iguais aos do Java (o .ui só define Ctrl+N e Ctrl+S) self.actionRest_Graphical_Path.setText("Reset Graphical Path") for action, shortcut in ((self.actionOpen_Project, "Ctrl+O"), (self.actionDelete_Project, "Ctrl+D"), @@ -85,6 +101,22 @@ def __init__(self): self.actionAbout.triggered.connect(lambda: AboutDialog(self).exec()) self.actionPacket_Format.triggered.connect(lambda: show_packet_format(self)) + # Imagem da malha inteira (mesmo a parte fora da área visível) + self.actionSave_Mesh_PNG = QAction("Save Mesh as PNG…", self) + self.menuFile.insertAction(self.actionExit, self.actionSave_Mesh_PNG) + self.actionSave_Mesh_PNG.triggered.connect(self.save_mesh_png) + + # Acompanhar um traffic_router.txt que o simulador ainda está gravando + self.actionFollow_Live_Trace = QAction("Follow Live Trace", self) + self.actionFollow_Live_Trace.setCheckable(True) + self.actionFollow_Live_Trace.setToolTip("At the end of traffic_router.txt, keep waiting for new packets " + "instead of stopping (for a simulation still running)") + self.menuFile.insertAction(self.actionSave_Mesh_PNG, self.actionFollow_Live_Trace) + self.actionFollow_Live_Trace.toggled.connect(self.set_follow_live_trace) + + # As janelas de análise mostram caminhos de pacotes na malha desta janela + path_view.set_handler(self.show_packet_path) + self.actionPrint_Link_Usage = QAction("Print Router Total Link Usage", self) self.menuEdit.addAction(self.actionPrint_Link_Usage) self.actionPrint_Link_Usage.triggered.connect(self.configure_link_usage_print) @@ -206,6 +238,31 @@ def open_filter(self): self.filter_window.raise_() self.filter_window.activateWindow() + def _show_window(self, window): + window.show() + window.raise_() + window.activateWindow() + + def open_traffic_statistics(self): + if self.simulation is None: + QMessageBox.warning(self, "Attention", "Please, load a debugging before") + return + window = self.traffic_statistics_window + if window is None or not window.isVisible(): + window = TrafficStatisticsWindow(self.mpconfig, self.mpsoc_information, self.simulation) + self.traffic_statistics_window = window + self._show_window(window) + + def open_application_timeline(self): + if self.simulation is None: + QMessageBox.warning(self, "Attention", "Please, load a debugging before") + return + window = self.timeline_window + if window is None or not window.isVisible(): + window = ApplicationTimelineWindow(self.mpconfig, self.simulation, self) + self.timeline_window = window + self._show_window(window) + def open_message_log(self): if self.filePath == "": QMessageBox.warning(self, "Attention", "Please, load a debugging before") @@ -280,6 +337,8 @@ def load_debug(self, caminho, title=None): self.actionServices_List.setEnabled(True) self.actionMessage_Log.setEnabled(True) self.actionTask_List.setEnabled(True) + self.actionTraffic_Statistics.setEnabled(True) + self.actionApplication_Timeline.setEnabled(True) self.mpconfig = MPSoCConfig.MPSoCConfig(self.filePath) @@ -404,6 +463,12 @@ def configure_link_usage_print(self): self.simulation.link_usage_router = address self.simulation.print_link_usage = mode.startswith("1") + def save_mesh_png(self): + if self.router_matrix_widget is None: + QMessageBox.warning(self, "Attention", "Please, load a debugging before") + return + export.export_png(self, self.router_matrix_widget, f"mesh_{self.simulation_ticks}_ticks") + def _router_label(self, address): if self.mpconfig.router_addressing == MPSoCConfig.MPSoCConfig.XY: return self.mpconfig.ham_address_to_xy_label(address) @@ -433,34 +498,22 @@ def build_router_matrix(self): for router in self.router_matrix_widget.routers.values(): router.clicked.connect(self.open_router_info) - # Primeira vez: cria um QScrollArea dentro do self.frame para - # comportar malhas maiores que a área visível da janela. - if self.frame_scroll_area is None: - self.frame_scroll_area = QScrollArea(self.frame) - # IMPORTANTE: widgetResizable=False. Com True, o Qt estica a - # matriz para preencher todo o frame e, sem stretch factor nas - # colunas/linhas do QGridLayout, esse espaço extra é distribuído - # como gaps entre os roteadores. Com False, a matriz mantém seu - # tamanho natural (roteadores colados) e o scroll aparece só se - # ela for maior que a área visível. - self.frame_scroll_area.setWidgetResizable(False) - self.frame_scroll_area.setAlignment(Qt.AlignmentFlag.AlignCenter) + # Nomes dos periféricos nas bordas, do YAML do testcase (sem ele, a borda é + # marcada quando passar o primeiro pacote vindo de um periférico) + peripherals, _yaml_path = testcase_files.read_peripherals(self.mpconfig) + self.router_matrix_widget.set_peripherals(peripherals) + # A malha é uma QGraphicsView (tem rolagem e zoom próprios) e ocupa + # todo o self.frame, substituindo a matriz de um debug anterior + frame_layout = self.frame.layout() + if frame_layout is None: frame_layout = QVBoxLayout(self.frame) frame_layout.setContentsMargins(0, 0, 0, 0) - frame_layout.addWidget(self.frame_scroll_area) - self.frame.setLayout(frame_layout) - - # Troca (ou define) o widget interno do scroll pela nova matriz, - # substituindo uma matriz antiga caso um novo arquivo seja aberto. - old_widget = self.frame_scroll_area.takeWidget() - if old_widget is not None: - old_widget.deleteLater() - - # Garante que a matriz assuma seu tamanho real (layout aplicado) - # antes de ser exibida dentro do scroll area sem resize forçado. - self.router_matrix_widget.adjustSize() - self.frame_scroll_area.setWidget(self.router_matrix_widget) + while frame_layout.count(): + old_widget = frame_layout.takeAt(0).widget() + if old_widget is not None: + old_widget.deleteLater() + frame_layout.addWidget(self.router_matrix_widget) # ========================================== # SIMULAÇÃO DOS ROTEADORES @@ -483,14 +536,19 @@ def start_simulation(self): self.simulation.packet_filter = self.filter_window.filter # Janelas de visão geral abertas passam a acompanhar o novo debug - for window in (self.communication_window, self.task_mapping_window, self.message_log_window): + for window in (self.communication_window, self.task_mapping_window, self.message_log_window, + self.traffic_statistics_window): if window is not None and window.isVisible(): window.set_information(self.mpconfig, self.mpsoc_information, self.simulation) + if self.timeline_window is not None and self.timeline_window.isVisible(): + self.timeline_window.set_information(self.mpconfig, self.simulation) self.simulation.time_changed.connect(self.update_simulation_time) self.simulation.packet_changed.connect(self.update_current_packet_table) self.simulation.running_changed.connect(self.on_simulation_running_changed) self.simulation.finished.connect(self.on_simulation_finished) self.simulation.unknown_service.connect(self.on_unknown_service) + self.simulation.waiting_for_data.connect(self.on_waiting_for_data) + self.simulation.set_follow(self.actionFollow_Live_Trace.isChecked()) self.update_simulation_time(0) self.reset_current_packet_table() @@ -571,6 +629,29 @@ def on_simulation_running_changed(self, running): def on_simulation_finished(self): self.statusbar.showMessage("End of traffic_router.txt reached", 5000) + def set_follow_live_trace(self, follow): + if self.simulation is not None: + self.simulation.set_follow(follow) + + def on_waiting_for_data(self, waiting): + if waiting: + self.statusbar.showMessage("Following traffic_router.txt: waiting for new packets…") + else: + self.statusbar.clearMessage() + + def show_packet_path(self, hops, target, highlight, description): + """Pinta na malha o caminho vindo de uma janela de análise (ver path_view).""" + if self.simulation is None: + return + self.simulation.show_path(hops, target, highlight) + if self.router_matrix_widget is not None and highlight is not None: + router = self.router_matrix_widget.get_router_by_address(highlight) + if router is not None: + self.router_matrix_widget.ensure_router_visible(router) + self.statusbar.showMessage(description or "Packet path", 10000) + self.raise_() + self.activateWindow() + def on_unknown_service(self, packet): if self.mpconfig.router_addressing == MPSoCConfig.MPSoCConfig.XY: router = self.mpconfig.ham_address_to_xy_label(packet.router_address) diff --git a/overview_windows.py b/overview_windows.py index f03a7cd..c0ae0e0 100644 --- a/overview_windows.py +++ b/overview_windows.py @@ -13,9 +13,11 @@ from PySide6.QtCore import Qt, QTimer from PySide6.QtGui import QColor, QPixmap -from PySide6.QtWidgets import (QHeaderView, QListWidget, QMessageBox, QTableWidgetItem, QVBoxLayout, +from PySide6.QtWidgets import (QComboBox, QHBoxLayout, QHeaderView, QLabel, QListWidget, QMessageBox, QTableWidgetItem, QVBoxLayout, QWidget) +import analysis +import export from commsUi import Ui_Form as Ui_CommunicationOverview from pe_matrix import PEMatrixWidget from slave_matrix import SlaveMatrixWidget @@ -152,6 +154,10 @@ def __init__(self, mpsoc_config, mpsoc_information, simulation=None): self.radioButton_2.setChecked(True) self.radioButton_4.setChecked(True) + export.install_export_actions(self, "communication_overview", png_widget=self) + export.install_export_actions(self.tableWidget, "communication_statistics", csv_view=self.tableWidget, + png_widget=self) + self.checkBox.toggled.connect(self._on_filter_changed) self.comboBox.currentIndexChanged.connect(self._on_filter_changed) for radio in (self.radioButton, self.radioButton_2, self.radioButton_3, @@ -282,6 +288,8 @@ def __init__(self, mpsoc_config, mpsoc_information, simulation=None): super().__init__(mpsoc_config, mpsoc_information, simulation) self.setupUi(self) + export.install_export_actions(self, "task_mapping", png_widget=self) + # O .ui repete "All tasks status" no terceiro botão self.radioButton_3.setText("Only Terminated") @@ -380,7 +388,8 @@ class MessageLogWindow(_LiveOverview): (pacote chegando ao roteador de destino) Mostra todos os pacotes já lidos e acrescenta os novos conforme a - simulação avança. + simulação avança. A caixa "Application" mostra só as mensagens de uma + aplicação (pelas tarefas de origem/destino do pacote). """ def __init__(self, mpsoc_config, mpsoc_information, simulation=None): @@ -388,15 +397,39 @@ def __init__(self, mpsoc_config, mpsoc_information, simulation=None): self.setWindowTitle("Message Log") self.resize(500, 600) + self.app_combo = QComboBox() + self.app_combo.currentIndexChanged.connect(lambda _index: self.rebuild()) + app_row = QHBoxLayout() + app_row.setContentsMargins(6, 6, 6, 0) + app_row.addWidget(QLabel("Application:")) + app_row.addWidget(self.app_combo, 1) + self.list_widget = QListWidget(self) self.list_widget.setUniformItemSizes(True) # listas longas continuam rápidas layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) + layout.addLayout(app_row) layout.addWidget(self.list_widget) + self._fill_app_combo() + self._packets_listed = 0 self.rebuild() + def _fill_app_combo(self): + self.known_apps = analysis.known_app_ids(self.mpsoc_config) + self.app_combo.blockSignals(True) + self.app_combo.clear() + self.app_combo.addItem("All", None) + for app in sorted(self.known_apps): + self.app_combo.addItem(analysis.app_label(self.mpsoc_config, app), app) + self.app_combo.blockSignals(False) + + def set_information(self, mpsoc_config, mpsoc_information, simulation=None): + self.mpsoc_config = mpsoc_config + self._fill_app_combo() + super().set_information(mpsoc_config, mpsoc_information, simulation) + def rebuild(self): self.list_widget.clear() self._packets_listed = 0 @@ -431,6 +464,9 @@ def refresh(self): scrollbar = self.list_widget.verticalScrollBar() at_bottom = scrollbar.value() == scrollbar.maximum() + app = self.app_combo.currentData() + if app is not None: + new_packets = [packet for packet in new_packets if app in analysis.packet_apps(packet, self.known_apps)] messages = [message for message in map(self._message, new_packets) if message is not None] self.list_widget.addItems(messages) diff --git a/path_view.py b/path_view.py new file mode 100644 index 0000000..29646fb --- /dev/null +++ b/path_view.py @@ -0,0 +1,35 @@ +""" +Mostrar na malha da janela principal o caminho de um pacote analisado. + +As janelas de análise (Traffic Statistics, gráfico de escalonamento...) +chamam show_message()/show_record(); a janela principal registra com +set_handler() a função que pinta as setas. Assim as janelas não precisam +conhecer a janela principal. +""" + +_handler = None + + +def set_handler(handler): + """handler(hops, target, highlight, description); hops = [(roteador, porta de entrada), ...].""" + global _handler + _handler = handler + + +def show_message(message, highlight=None, description=""): + """Caminho registrado de uma analysis.Message, destacando `highlight` (padrão: onde o pacote está).""" + if _handler is None: + return False + hops = [(router, port) for router, port, _time in message.path] + if highlight is None: + highlight = message.last_router + _handler(hops, message.target, highlight, description) + return True + + +def show_record(router, port, target, description=""): + """Um único registro do trace (quando o pacote não foi identificado).""" + if _handler is None: + return False + _handler([(router, port)], target, router, description) + return True diff --git a/pyproject.toml b/pyproject.toml index 3f7833c..565a567 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,3 +6,12 @@ dependencies = [ "pyside6>=6.7", "sortedcontainers>=2.4.0", ] + +[dependency-groups] +dev = [ + "pytest>=9.1.1", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] diff --git a/router_info_window.py b/router_info_window.py index 0519b39..0c2c876 100644 --- a/router_info_window.py +++ b/router_info_window.py @@ -17,13 +17,16 @@ import os from dataclasses import dataclass -from PySide6.QtCore import QEvent, QRect, Qt, QTimer +from PySide6.QtCore import QEvent, QRect, Qt, QTimer, Signal from PySide6.QtGui import QColor, QFont, QFontDatabase, QPainter, QPen, QPixmap from PySide6.QtWidgets import (QAbstractItemView, QComboBox, QGridLayout, QGroupBox, QHBoxLayout, QHeaderView, QInputDialog, QLabel, QMessageBox, QPlainTextEdit, - QPushButton, QScrollArea, QTableWidget, QTableWidgetItem, QTabWidget, + QPushButton, QScrollArea, QSplitter, QTableWidget, QTableWidgetItem, QTabWidget, QVBoxLayout, QWidget) +import analysis +import export +import path_view from overview_windows import _LiveOverview, _router_label from util.MPSoCConfig import MPSoCConfig @@ -65,6 +68,14 @@ def _task_name(mpsoc_config, task_id): return mpsoc_config.task_name_hash.get(task_id, str(task_id)) +def _slice_task(mpsoc_config, task_id): + if task_id < 0: + return "-" + if task_id >= analysis.KERNEL_TASK_ID: + return "kernel" + return _task_name(mpsoc_config, task_id) + + def _pe_xy_address(mpsoc_config, router_address): """Endereço (x << 8) | y do PE, usado nos nomes dos arquivos de log.""" return mpsoc_config.ham_to_xy_addr(router_address) @@ -155,6 +166,8 @@ def _build_applications_tab(self): self.task_table.setToolTip("Double click a task to see its messages.\n" "ALLOCATED* = task still running") self.task_table.cellDoubleClicked.connect(self._open_task_info) + export.install_export_actions(self.task_table, f"router_{self.router_address}_applications", + csv_view=self.task_table) return self.task_table def _update_task_table(self): @@ -227,6 +240,8 @@ def _build_traffic_tab(self): self.traffic_table = _read_only_table(["Input Port", "Total Volume", "Service Volume", "Percentual"]) self.traffic_table.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection) + export.install_export_actions(self.traffic_table, f"router_{self.router_address}_traffic", + csv_view=self.traffic_table) layout = QVBoxLayout(tab) layout.addWidget(filter_box) @@ -407,6 +422,7 @@ class SchedulingEvent: start: int finish: int rect: QRect = None # posição no último desenho, para o clique + pe: int = -1 # endereço hamiltoniano do PE que executou o trecho def read_cpu_events(debug_dir): @@ -464,6 +480,9 @@ class SchedulingGraph(QWidget): - Ctrl + roda do mouse: zoom horizontal """ + # Trecho clicado (SchedulingEvent) ou None + selection_changed = Signal(object) + TOP = 50 # INITI_Y ROW_HEIGHT = 26 # NAME_SPACE CPU_COLOR = QColor(21, 119, 40) @@ -511,15 +530,17 @@ def _load_events(self): if self.app_filter == -1: pe = _pe_report_address(config, self.router_address) - slices = read_scheduling_report(path, pe).get(pe, []) + slices = [(self.router_address, *piece) for piece in read_scheduling_report(path, pe).get(pe, [])] else: # Tarefas da aplicação em todos os PEs (os eventos de CPU têm bits acima do 16) - slices = [piece for pe_slices in read_scheduling_report(path).values() + def ham(report_pe): + return config.xy_to_ham_addr(report_pe) if config.router_addressing == MPSoCConfig.XY else report_pe + slices = [(ham(report_pe), *piece) for report_pe, pe_slices in read_scheduling_report(path).items() for piece in pe_slices if piece[0] >> 16 == 0 and piece[0] >> 8 == self.app_filter] - return [SchedulingEvent(code, self._event_name(code), start, finish) - for code, start, finish in slices] + return [SchedulingEvent(code, self._event_name(code), start, finish, pe=pe) + for pe, code, start, finish in slices] def _event_name(self, code): if code in self.cpu_events: @@ -709,6 +730,7 @@ def mouseReleaseEvent(self, event): if event.button() == Qt.MouseButton.LeftButton: self.selected = self._event_at(event.position().toPoint()) self.update() + self.selection_changed.emit(self.selected) def wheelEvent(self, event): if not event.modifiers() & Qt.KeyboardModifier.ControlModifier: @@ -724,6 +746,8 @@ class SchedulingGraphWindow(QWidget): def __init__(self, mpsoc_config, router_address, app_filter=-1): super().__init__() self.graph = SchedulingGraph(mpsoc_config, router_address, app_filter) + name = f"scheduling_{router_address}" if app_filter == -1 else f"scheduling_app_{app_filter}" + export.install_export_actions(self.graph, name, png_widget=self.graph) if app_filter == -1: self.setWindowTitle(f"Scheduling Graph {_router_label(mpsoc_config, router_address)}") @@ -735,14 +759,79 @@ def __init__(self, mpsoc_config, router_address, app_filter=-1): scroll.setWidgetResizable(True) scroll.setWidget(self.graph) scroll.horizontalScrollBar().setSingleStep(200) + + # Pacotes do PE durante o trecho clicado (depuração hardware + software) + self.mpsoc_config = mpsoc_config + self._tracker = None + self._slice_messages = [] + self.slice_label = QLabel("Click a slice of the graph to see the packets the PE received and sent " + "during it (e.g. the packet that caused an interruption).") + self.slice_label.setWordWrap(True) + self.slice_table = _read_only_table(["Time (tick)", "Direction", "From", "To", "Service", "Flits", + "Latency (cycles)", "Tasks"]) + self.slice_table.setToolTip("Double click a packet to show its path on the main window mesh") + self.slice_table.cellDoubleClicked.connect(self._show_slice_message) + export.install_export_actions(self.slice_table, f"{name}_slice_packets", csv_view=self.slice_table) + packets_panel = QWidget() + packets_layout = QVBoxLayout(packets_panel) + packets_layout.addWidget(self.slice_label) + packets_layout.addWidget(self.slice_table) + self.graph.selection_changed.connect(self._show_slice_packets) + + splitter = QSplitter(Qt.Orientation.Vertical) + splitter.addWidget(scroll) + splitter.addWidget(packets_panel) + layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(scroll) + layout.addWidget(splitter) screen = self.screen().availableGeometry() - self.resize(screen.width(), min(screen.height(), self.graph.minimumHeight() + 30)) + graph_height = min(screen.height() - 240, self.graph.minimumHeight() + 30) + splitter.setSizes([graph_height, 220]) + self.resize(screen.width(), min(screen.height(), graph_height + 240)) self.move(screen.topLeft()) + def _all_messages(self): + # O trace inteiro é analisado uma vez, no primeiro clique + if self._tracker is None: + self._tracker = analysis.track_all(self.mpsoc_config, analysis.read_all_packets(self.mpsoc_config)) + return self._tracker.messages + + def _show_slice_packets(self, event): + config = self.mpsoc_config + if event is None: + self._slice_messages = [] + self.slice_table.setRowCount(0) + return + + self._slice_messages = analysis.messages_in_slice(self._all_messages(), event.pe, event.start, event.finish) + self.slice_label.setText( + f"{event.name} on PE {_router_label(config, event.pe)}, {event.start} → {event.finish} ticks: " + f"{len(self._slice_messages)} packet(s). Received = delivered to the PE up to " + f"{analysis.SLICE_LOOKBACK} cycles before the slice or during it; sent = injected during it.") + + table = self.slice_table + table.setRowCount(len(self._slice_messages)) + for row, (message, direction) in enumerate(self._slice_messages): + time = message.delivered if direction == "received" else message.injected + source = _router_label(config, message.source) + if message.from_peripheral: + source = f"Periph. @ {source}" + tasks = (f"{_slice_task(config, message.task_source)} → {_slice_task(config, message.task_target)}" + if message.task_source >= 0 or message.task_target >= 0 else "-") + values = (str(time), direction, source, _router_label(config, message.target), + config.get_string_service_name(message.service), str(message.flits), + str(message.latency) if message.delivered >= 0 else "not delivered", tasks) + for column, value in enumerate(values): + table.setItem(row, column, QTableWidgetItem(value)) + + def _show_slice_message(self, row, _column): + if 0 <= row < len(self._slice_messages): + message, _direction = self._slice_messages[row] + path_view.show_message(message, description=f"Packet {self.mpsoc_config.get_string_service_name(message.service)} " + f"from the scheduling graph") + # ========================================== # TASK INFORMATION @@ -798,6 +887,7 @@ def caption(text): buttons.addStretch() self.message_table = _read_only_table(["Time: Requested / Delivered", "Remote task", "Delivered", "Num"]) + export.install_export_actions(self.message_table, f"task_{task_id}_messages", csv_view=self.message_table) header = self.message_table.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) for column in (1, 2, 3): diff --git a/router_matrix.py b/router_matrix.py index 750a698..f4a0b95 100644 --- a/router_matrix.py +++ b/router_matrix.py @@ -2,9 +2,10 @@ """ router_matrix.py -Monta a matriz gráfica de roteadores (grade NoC/MPSoC) dentro de um QWidget, -usando como base o "Ui_Form" gerado a partir de Roteador.ui (arquivo -Roteador.py) para desenhar cada roteador individual. +Desenha a matriz de roteadores (grade NoC/MPSoC) com QGraphicsView / +QGraphicsScene: corpo dos roteadores, setas dos enlaces, porta local e +periféricos são desenhados em vetor pelo próprio Qt (sem imagens), então a +malha pode ser ampliada/reduzida sem perder nitidez. Semântica dos campos de MPSoCConfig (igual ao formato de configuração do Memphis: mpsoc_dimension é o tamanho TOTAL da malha, cluster_dimension é o @@ -15,433 +16,780 @@ gerenciamento dentro da malha), usado só para desenhar a borda de separação entre clusters -Ou seja, o total de roteadores é sempre mpsoc_x * mpsoc_y — cluster_x e -cluster_y NÃO multiplicam esse total, apenas dividem a malha em blocos -visuais (com borda) de cluster_x por cluster_y roteadores cada. +A numeração de cada roteador segue a convenção do debugger original: o +roteador "0x0" fica no canto inferior esquerdo e o eixo Y cresce para cima. -A numeração de cada roteador segue a mesma convenção do print de -referência: o roteador "0x0" fica no canto inferior esquerdo e o eixo Y -cresce para cima (0x0, 0x1, 0x2, ... de baixo para cima). +Cada enlace entre dois roteadores vizinhos tem, por canal (HIGH/LOW), uma +seta em cada sentido. A mesma seta é, ao mesmo tempo, a saída de um roteador +e a entrada do vizinho (ex.: EAST_OUT_HIGH de 0x0 == WEST_IN_HIGH de 1x0), +por isso os dois roteadores guardam a mesma ArrowItem. Nas bordas da malha, +as setas ligam o roteador à caixa do periférico (desenhada fora do roteador). Durante a simulação, cada roteador pinta de vermelho as setas por onde o pacote atual passou (paint_arrow) e mostra a taxa de uso de cada enlace (update_throughput), igual ao Roteador.java do debugger original. + +Zoom: Ctrl + roda do mouse (ou gesto de pinça), Ctrl +/-/0, os botões no +canto da vista ou duplo clique no fundo (ajusta a malha à janela). Arrastar +o fundo move a vista. """ import math -import os -from PySide6.QtCore import QEvent, QObject, QRect, Qt, Signal -from PySide6.QtGui import QColor, QFont, QImage, QPainter, QPixmap -from PySide6.QtWidgets import QFrame, QGridLayout, QWidget +from PySide6.QtCore import QEvent, QPointF, QRectF, QSize, Qt, QTimer, Signal +from PySide6.QtGui import (QBrush, QColor, QFont, QFontMetricsF, QImage, QKeySequence, QPainter, + QPainterPath, QPen, QPolygonF, QShortcut) +from PySide6.QtWidgets import (QFrame, QGraphicsItem, QGraphicsObject, QGraphicsRectItem, QGraphicsScene, + QGraphicsView, QHBoxLayout, QLabel, QToolButton) import theme -from Roteador import Ui_Form from util.MPSoCConfig import MPSoCConfig -IMAGES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "images") +# ========================================== +# GEOMETRIA (unidades da cena = pixels com zoom 100%) +# ========================================== +BODY = 130 # lado do corpo do roteador +GAP = 64 # espaço entre dois roteadores (onde ficam as setas) +PITCH = BODY + GAP +ARROW_OFFSET = 8 # distância de cada seta do par ao centro do canal +ARROW_MARGIN = 4 # folga entre a ponta da seta e o corpo do roteador +SHAFT_WIDTH = 4.5 +HEAD_LENGTH = 11 +HEAD_WIDTH = 12 + +# Caixa do periférico: mais larga nas bordas norte/sul (setas verticais) e mais +# alta nas bordas leste/oeste, para cobrir os dois pares de setas +PERIPHERAL_NS = QSize(110, 38) +PERIPHERAL_EW = QSize(100, 90) + +ZOOM_MIN, ZOOM_MAX, ZOOM_STEP = 0.1, 8.0, 1.25 + +# Cor do corpo do roteador de acordo com o tipo de PE (as mesmas das imagens antigas) +_ROUTER_COLORS = { + MPSoCConfig.SLAVE: "#b7dde8", + MPSoCConfig.CLUSTER_MASTER: "#c4d6a0", + MPSoCConfig.GLOBAL_MASTER: "#f9c499", +} +_PE_TYPE_NAMES = { + MPSoCConfig.SLAVE: "Slave PE", + MPSoCConfig.CLUSTER_MASTER: "Cluster manager PE", + MPSoCConfig.GLOBAL_MASTER: "Global manager PE", +} +_BODY_BORDER = "#2d2e2f" +_HIGHLIGHT = "#ff8c00" + +_SIDE_NAMES = {"N": "north", "S": "south", "E": "east", "W": "west"} +_OPPOSITE = {"N": "S", "S": "N", "E": "W", "W": "E"} +# Direção "para fora" de cada lado, em coordenadas da cena (Y cresce para baixo) +_OUTWARD = {"N": (0, -1), "S": (0, 1), "E": (1, 0), "W": (-1, 0)} + +HIGH, LOW = MPSoCConfig.HIGH, MPSoCConfig.LOW + +# (lado, canal) -> (seta de entrada, seta de saída) nas constantes do MPSoCConfig +_SIDE_ARROWS = { + ("E", HIGH): (MPSoCConfig.EAST_IN_HIGH, MPSoCConfig.EAST_OUT_HIGH), + ("E", LOW): (MPSoCConfig.EAST_IN_LOW, MPSoCConfig.EAST_OUT_LOW), + ("W", HIGH): (MPSoCConfig.WEST_IN_HIGH, MPSoCConfig.WEST_OUT_HIGH), + ("W", LOW): (MPSoCConfig.WEST_IN_LOW, MPSoCConfig.WEST_OUT_LOW), + ("N", HIGH): (MPSoCConfig.NORTH_IN_HIGH, MPSoCConfig.NORTH_OUT_HIGH), + ("N", LOW): (MPSoCConfig.NORTH_IN_LOW, MPSoCConfig.NORTH_OUT_LOW), + ("S", HIGH): (MPSoCConfig.SOUTH_IN_HIGH, MPSoCConfig.SOUTH_OUT_HIGH), + ("S", LOW): (MPSoCConfig.SOUTH_IN_LOW, MPSoCConfig.SOUTH_OUT_LOW), +} -# Cache das imagens já carregadas (QPixmap só pode ser criado com a QApplication ativa) -_pixmap_cache: dict[str, QPixmap] = {} +# Porta física -> (lado, canal) do rótulo com a taxa de uso do enlace +_PORT_SIDES = { + MPSoCConfig.EAST0: ("E", HIGH), MPSoCConfig.EAST1: ("E", LOW), + MPSoCConfig.WEST0: ("W", HIGH), MPSoCConfig.WEST1: ("W", LOW), + MPSoCConfig.NORTH0: ("N", HIGH), MPSoCConfig.NORTH1: ("N", LOW), + MPSoCConfig.SOUTH0: ("S", HIGH), MPSoCConfig.SOUTH1: ("S", LOW), +} +_LOCAL_PORTS = (MPSoCConfig.LOCAL0, MPSoCConfig.LOCAL1) -def _white_to_alpha(image: QImage) -> QImage: +def _channel_positions(mpsoc_config) -> dict[int, float]: """ - Remove o fundo branco das imagens das setas (os PNGs não têm canal - alfa), para que fiquem legíveis sobre o fundo escuro do modo escuro. - - A cor mais distante do branco é tomada como a cor da seta; os pixels - mais claros (fundo e serrilhado da borda) viram essa cor com opacidade - proporcional à distância do branco. + Posição (ao longo do lado do roteador) do centro de cada canal. Com um + canal físico só existem as portas ímpares (LOW, ver read_traffic_data), + que ficam no meio do lado. """ - image = image.convertToFormat(QImage.Format.Format_ARGB32) - width, height = image.width(), image.height() - - def distance(color: QColor) -> int: - return 255 - min(color.red(), color.green(), color.blue()) - - pixels = [[image.pixelColor(x, y) for x in range(width)] for y in range(height)] - base = max((color for row in pixels for color in row), key=distance) - base_distance = distance(base) or 1 - - for y, row in enumerate(pixels): - for x, color in enumerate(row): - alpha = distance(color) / base_distance - if alpha < 1: - faded = QColor(base) - faded.setAlphaF(alpha) - image.setPixelColor(x, y, faded) - - return image - - -def _pixmap(name: str) -> QPixmap: - if name not in _pixmap_cache: - image = QImage(os.path.join(IMAGES_DIR, name)) - # As imagens do corpo do roteador são opacas e não têm fundo branco - if not name.startswith("Router"): - image = _white_to_alpha(image) - _pixmap_cache[name] = QPixmap.fromImage(image) - return _pixmap_cache[name] - - -# Imagem do roteador de acordo com o tipo de PE -_ROUTER_IMAGES = { - MPSoCConfig.SLAVE: "Router.png", - MPSoCConfig.CLUSTER_MASTER: "Router_cluster_master.png", - MPSoCConfig.GLOBAL_MASTER: "Router_master.png", -} + if mpsoc_config is not None and mpsoc_config.channel_number == 1: + return {LOW: BODY * 0.5} + return {HIGH: BODY * 0.3, LOW: BODY * 0.7} -# Seta -> (widget do Roteador.ui, imagem quando pintada de vermelho) -_ARROWS = { - MPSoCConfig.EAST_IN_HIGH: ("in_east_HIGH", "red_left.png"), - MPSoCConfig.EAST_OUT_HIGH: ("out_east_HIGH", "red_undirected_h.png"), - MPSoCConfig.EAST_IN_LOW: ("in_east_LOW", "red_left.png"), - MPSoCConfig.EAST_OUT_LOW: ("out_east_LOW", "red_undirected_h.png"), - MPSoCConfig.NORTH_IN_HIGH: ("in_north_HIGH", "red_down.png"), - MPSoCConfig.NORTH_OUT_HIGH: ("out_north_HIGH", "red_undirected_v.png"), - MPSoCConfig.NORTH_IN_LOW: ("in_north_LOW", "red_down.png"), - MPSoCConfig.NORTH_OUT_LOW: ("out_north_LOW", "red_undirected_v.png"), - MPSoCConfig.WEST_IN_HIGH: ("in_weast_HIGH", "red_right.png"), - MPSoCConfig.WEST_OUT_HIGH: ("out_weast_HIGH", "red_undirected_h.png"), - MPSoCConfig.WEST_IN_LOW: ("in_weast_LOW", "red_right.png"), - MPSoCConfig.WEST_OUT_LOW: ("out_weast_LOW", "red_undirected_h.png"), - MPSoCConfig.SOUTH_IN_HIGH: ("in_south_HIGH", "red_up.png"), - MPSoCConfig.SOUTH_OUT_HIGH: ("out_south_HIGH", "red_undirected_v.png"), - MPSoCConfig.SOUTH_IN_LOW: ("in_south_LOW", "red_up.png"), - MPSoCConfig.SOUTH_OUT_LOW: ("out_south_LOW", "red_undirected_v.png"), - MPSoCConfig.LOCAL_IN: ("local_in", "red_local_in.png"), - MPSoCConfig.LOCAL_OUT: ("local_out", "red_local_out.png"), -} -# Setas (e imagens em estado normal) de cada lado do roteador; só são -# desenhadas quando existe um vizinho naquele lado -_WEST_ARROWS = {"out_weast_HIGH": "undirected_h.png", "in_weast_HIGH": "right.png", - "out_weast_LOW": "undirected_h.png", "in_weast_LOW": "right.png"} -_EAST_ARROWS = {"in_east_LOW": "left.png", "out_east_LOW": "undirected_h.png", - "in_east_HIGH": "left.png", "out_east_HIGH": "undirected_h.png"} -_SOUTH_ARROWS = {"in_south_LOW": "up.png", "out_south_LOW": "undirected_v.png", - "in_south_HIGH": "up.png", "out_south_HIGH": "undirected_v.png"} -_NORTH_ARROWS = {"out_north_HIGH": "undirected_v.png", "in_north_HIGH": "down.png", - "out_north_LOW": "undirected_v.png", "in_north_LOW": "down.png"} - -# Porta física -> rótulo com a taxa de uso do enlace -_PORT_LABELS = { - MPSoCConfig.EAST0: "east_HIGH_Label", - MPSoCConfig.EAST1: "east_LOW_Label", - MPSoCConfig.NORTH0: "north_HIGH_Label", - MPSoCConfig.NORTH1: "north_LOW_Label", - MPSoCConfig.WEST0: "weast_HIGH_Label", - MPSoCConfig.WEST1: "weast_LOW_Label", - MPSoCConfig.SOUTH0: "south_HIGH_Label", - MPSoCConfig.SOUTH1: "south_LOW_Label", - MPSoCConfig.LOCAL0: "local_Label", - MPSoCConfig.LOCAL1: "local_Label", -} +def _percent_font() -> QFont: + # "Andale Mono" nem sempre está instalada; qualquer monoespaçada serve + font = QFont("Monospace") + font.setStyleHint(QFont.StyleHint.Monospace) + font.setPixelSize(10) + return font -class _ImagePainter(QObject): - """ - Desenha uma imagem esticada sobre os QWidgets "vazios" do Roteador.ui - (setas e corpo do roteador), no lugar do UJPanelImagem do Java. - """ +def _format_load(value: float) -> str: + return f"{min(value, 99.99):05.2f}%" + + +class _MeshScene(QGraphicsScene): + """Cena da malha; guarda as cores do tema atual, lidas pelos itens ao pintar.""" def __init__(self, parent=None): super().__init__(parent) - self.images: dict[QWidget, str] = {} - - def set_image(self, widget: QWidget, name: str | None) -> None: - """Define a imagem do widget; None deixa o widget vazio.""" - if self.images.get(widget) == name: - return - if widget not in self.images: - widget.installEventFilter(self) - self.images[widget] = name - widget.update() - - def eventFilter(self, obj, event): - if event.type() == QEvent.Type.Paint and obj in self.images: - if self.images[obj] is None: - return True - painter = QPainter(obj) - painter.drawPixmap(obj.rect(), _pixmap(self.images[obj])) - painter.end() - return True - return super().eventFilter(obj, event) + self.colors = theme.matrix_colors(False) -def _matrix_stylesheet(dark: bool) -> str: - """ - Folha de estilo da grade inteira (aplicada no RouterMatrixWidget e - herdada pelos roteadores). Os rótulos dentro do corpo do roteador são - sempre pretos, pois o corpo tem cor clara nos dois temas; o % da porta - local fica fora do corpo e acompanha o tema. +# ========================================== +# ITENS DA CENA +# ========================================== +class ArrowItem(QGraphicsItem): """ - colors = theme.matrix_colors(dark) - return f""" - #routerMatrix {{ - background-color: {colors.canvas}; - }} - #routerMatrix QLabel {{ - background-color: transparent; - color: #000000; - }} - #routerMatrix QLabel#local_Label {{ - color: {colors.text}; - }} - #routerMatrix QFrame[clusterFrame="true"] {{ - background-color: transparent; - border: 1px solid {colors.cluster_border}; - }} + Seta de um enlace, de `start` até `end`. Fica cinza normalmente e vermelha + quando o pacote atual passou por ela. Setas `optional` (bordas da malha sem + periférico conhecido) só aparecem enquanto estão pintadas. """ - -class RouterWidget(QWidget, Ui_Form): + def __init__(self, start: QPointF, end: QPointF, optional: bool = False, tooltip: str = ""): + super().__init__() + self.painted = False + self.optional = optional + + delta = end - start + self._length = math.hypot(delta.x(), delta.y()) + self.setPos(start) + self.setRotation(math.degrees(math.atan2(delta.y(), delta.x()))) + self.setZValue(1) + if tooltip: + self.setToolTip(tooltip) + + length, shaft, head = self._length, SHAFT_WIDTH / 2, HEAD_WIDTH / 2 + neck = length - HEAD_LENGTH + self._polygon = QPolygonF([ + QPointF(0, -shaft), QPointF(neck, -shaft), QPointF(neck, -head), QPointF(length, 0), + QPointF(neck, head), QPointF(neck, shaft), QPointF(0, shaft), + ]) + self._update_visibility() + + def set_painted(self, painted: bool) -> None: + if painted != self.painted: + self.painted = painted + self._update_visibility() + self.update() + + def set_optional(self, optional: bool) -> None: + self.optional = optional + self._update_visibility() + + def _update_visibility(self) -> None: + self.setVisible(self.painted or not self.optional) + + def boundingRect(self) -> QRectF: + return QRectF(0, -HEAD_WIDTH / 2 - 1, self._length + 1, HEAD_WIDTH + 2) + + def paint(self, painter: QPainter, option, widget=None) -> None: + colors = self.scene().colors + painter.setPen(Qt.PenStyle.NoPen) + painter.setBrush(QColor(colors.arrow_active if self.painted else colors.arrow)) + painter.drawPolygon(self._polygon) + + +class PeripheralItem(QGraphicsItem): + """Caixa com o nome do periférico ligado a uma porta de borda, fora do roteador.""" + + def __init__(self, rect: QRectF, name: str, router_label: str, side: str): + super().__init__() + self._rect = rect + self.name = name + self.setZValue(1) + self.setToolTip(f"Peripheral {name} at the {_SIDE_NAMES[side]} port of router {router_label}") + self._font = QFont() + self._font.setBold(True) + self._font.setPixelSize(12) + + def boundingRect(self) -> QRectF: + return self._rect.adjusted(-1, -1, 1, 1) + + def paint(self, painter: QPainter, option, widget=None) -> None: + colors = self.scene().colors + color = QColor(colors.peripheral_text) + painter.setPen(QPen(color, 1.5)) + painter.setBrush(QColor(colors.peripheral_fill)) + painter.drawRoundedRect(self._rect, 6, 6) + + painter.setFont(self._font) + text_rect = self._rect.adjusted(6, 4, -6, -4) + text = QFontMetricsF(self._font).elidedText(self.name, Qt.TextElideMode.ElideRight, text_rect.width()) + painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter, text) + + +class RouterItem(QGraphicsObject): """ - Um único roteador da malha. Reaproveita o layout gerado em Roteador.py - e apenas customiza o texto central (routerLabel) para exibir a posição - "XxY" do roteador dentro da malha, igual ao print de referência. + Um roteador da malha: corpo colorido pelo tipo de PE, nome no centro, + porcentagem de uso de cada enlace junto ao lado correspondente e, no canto + superior esquerdo (fora do corpo), as setas e a porcentagem da porta local. + As setas dos enlaces são itens próprios (ArrowItem), compartilhados com o + vizinho, e ficam em `self.arrows`. """ # Emitido com o endereço hamiltoniano quando o roteador é clicado clicked = Signal(int) - def __init__(self, pos_x: int, pos_y: int, parent=None, - mpsoc_x: int = 1, mpsoc_y: int = 1, mpsoc_config=None): - super().__init__(parent) - self.setupUi(self) - + def __init__(self, pos_x: int, pos_y: int, mpsoc_x: int = 1, mpsoc_y: int = 1, mpsoc_config=None): + super().__init__() self.pos_x = pos_x self.pos_y = pos_y self.mpsoc_x = mpsoc_x self.mpsoc_y = mpsoc_y - self.need_reset = True + self.need_reset = False + self.highlighted = False + self._hovered = False if mpsoc_config is not None: self.router_address = mpsoc_config.xy_to_ham_addr((pos_x << 8) | pos_y) self.pe_type = mpsoc_config.get_pe_type(self.router_address) - if mpsoc_config.channel_number == 1: - self._set_single_channel_geometry() else: self.router_address = pos_y * mpsoc_x + pos_x self.pe_type = MPSoCConfig.SLAVE + self.xy_label = f"{pos_x}x{pos_y}" if mpsoc_config is not None and mpsoc_config.router_addressing == MPSoCConfig.HAMILTONIAN: - self.routerLabel.setText(str(self.router_address)) + self.label = str(self.router_address) else: - self.routerLabel.setText(f"{pos_x}x{pos_y}") - - self.router.setCursor(Qt.CursorShape.PointingHandCursor) - self._painter = _ImagePainter(self) - - # "Andale Mono" nem sempre está instalada; usa qualquer fonte - # monoespaçada em tamanho fixo para o "00.00%" caber no rótulo - percent_font = QFont("Monospace") - percent_font.setStyleHint(QFont.StyleHint.Monospace) - percent_font.setPixelSize(10) - for label_name in set(_PORT_LABELS.values()): - getattr(self, label_name).setFont(percent_font) - - # O Roteador.ui fixa fundo branco no Form inteiro; as cores passam a - # vir da folha de estilo da matriz (_matrix_stylesheet), que segue o tema - self.setStyleSheet("") - - # Mesma posição do Java: acima do corpo do roteador, sem cruzar a borda - self.local_Label.setGeometry(QRect(21, 16, 40, 13)) - - self.reset_arrows() - - def _set_single_channel_geometry(self) -> None: - """Com apenas um canal físico, as setas LOW ocupam o centro de cada lado.""" - self.north_LOW_Label.setGeometry(QRect(70, 0, 40, 20)) - self.in_north_LOW.setGeometry(QRect(110, 0, 20, 30)) - self.out_north_LOW.setGeometry(QRect(77, 0, 6, 30)) - - self.east_LOW_Label.setGeometry(QRect(90, 30, 40, 20)) - self.in_east_LOW.setGeometry(QRect(160, 63, 33, 20)) - self.out_east_LOW.setGeometry(QRect(160, 107, 30, 6)) - - self.south_LOW_Label.setGeometry(QRect(40, 110, 40, 20)) - self.in_south_LOW.setGeometry(QRect(70, 160, 20, 30)) - self.out_south_LOW.setGeometry(QRect(117, 160, 6, 30)) - - self.weast_LOW_Label.setGeometry(QRect(1, 70, 40, 20)) - self.in_weast_LOW.setGeometry(QRect(0, 100, 30, 20)) - self.out_weast_LOW.setGeometry(QRect(0, 70, 30, 6)) + self.label = self.xy_label + + self._channels = _channel_positions(mpsoc_config) + + # Constante do MPSoCConfig (*_IN_* / *_OUT_* / LOCAL_*) -> ArrowItem + self.arrows: dict[int, ArrowItem] = {} + # Periféricos ligados às portas de borda: lado ("N", "S", "E", "W") -> nome + self.peripherals: dict[str, str] = {} + # Callback da matriz que desenha a caixa do periférico + self._peripheral_added = None + + self._loads = {key: _format_load(0) for key in _PORT_SIDES.values()} + self._local_load = _format_load(0) + + self._name_font = QFont("Arial") + self._name_font.setBold(True) + self._name_font.setPixelSize(19) + self._percent_font = _percent_font() + + self.setZValue(2) + self.setAcceptHoverEvents(True) + self.setCursor(Qt.CursorShape.PointingHandCursor) + pe_name = _PE_TYPE_NAMES.get(self.pe_type, "PE") + self.setToolTip(f"Router {self.label} ({self.xy_label}, address {self.router_address})\n" + f"{pe_name} - click for details") + + # ---------- desenho ---------- + def body_rect(self) -> QRectF: + return QRectF(0, 0, BODY, BODY) + + def boundingRect(self) -> QRectF: + # Inclui a porcentagem da porta local (canto superior esquerdo, fora do + # corpo) e a moldura de destaque + return QRectF(-GAP, -GAP, BODY + GAP + 6, BODY + GAP + 6) + + def shape(self) -> QPainterPath: + # Só o corpo recebe cliques e hover + path = QPainterPath() + path.addRect(self.body_rect()) + return path + + def _label_rect(self, side: str, position: float) -> QRectF: + width, height, pad = 44, 14, 4 + if side == "E": + return QRectF(BODY - width - pad, position - height / 2, width, height) + if side == "W": + return QRectF(pad, position - height / 2, width, height) + if side == "N": + return QRectF(position - width / 2, pad, width, height) + return QRectF(position - width / 2, BODY - height - pad, width, height) + + def paint(self, painter: QPainter, option, widget=None) -> None: + colors = self.scene().colors + body = self.body_rect() + + painter.setPen(QPen(QColor(_BODY_BORDER), 2.5 if self._hovered else 1.2)) + painter.setBrush(QColor(_ROUTER_COLORS.get(self.pe_type, _ROUTER_COLORS[MPSoCConfig.SLAVE]))) + painter.drawRect(body) + + # O corpo tem cor clara nos dois temas: textos internos sempre pretos + painter.setPen(QColor("#000000")) + painter.setFont(self._name_font) + painter.drawText(body, Qt.AlignmentFlag.AlignCenter, self.label) + + painter.setFont(self._percent_font) + for (side, channel), text in self._loads.items(): + position = self._channels.get(channel) + if position is None: + continue + align = {"E": Qt.AlignmentFlag.AlignRight, "W": Qt.AlignmentFlag.AlignLeft}.get( + side, Qt.AlignmentFlag.AlignHCenter) + painter.drawText(self._label_rect(side, position), align | Qt.AlignmentFlag.AlignVCenter, text) + + # Porta local: acima das setas diagonais, no canto fora do corpo + painter.setPen(QColor(colors.text)) + painter.drawText(QRectF(-GAP + 2, -GAP + 1, 46, 13), Qt.AlignmentFlag.AlignLeft, self._local_load) + + if self.highlighted: + painter.setPen(QPen(QColor(_HIGHLIGHT), 4)) + painter.setBrush(Qt.BrushStyle.NoBrush) + painter.drawRoundedRect(body.adjusted(-4, -4, 4, 4), 6, 6) + + # ---------- interação ---------- + def hoverEnterEvent(self, event): + self._hovered = True + self.update() + super().hoverEnterEvent(event) + + def hoverLeaveEvent(self, event): + self._hovered = False + self.update() + super().hoverLeaveEvent(event) + + def mousePressEvent(self, event): + # Aceitar o clique impede que a vista comece a arrastar a partir do roteador + if event.button() == Qt.MouseButton.LeftButton: + event.accept() + else: + super().mousePressEvent(event) def mouseReleaseEvent(self, event): - if self.router.geometry().contains(event.position().toPoint()): + if event.button() == Qt.MouseButton.LeftButton and self.body_rect().contains(event.pos()): self.clicked.emit(self.router_address) super().mouseReleaseEvent(event) + # ---------- estado da simulação ---------- def reset_arrows(self) -> None: """Volta todas as setas para o estado normal (sem pacote passando).""" self.need_reset = False - - self._painter.set_image(self.router, _ROUTER_IMAGES.get(self.pe_type, "Router.png")) - self._painter.set_image(self.local_in, "local_in.png") - self._painter.set_image(self.local_out, "local_out.png") - - # Setas de bordas sem vizinho ficam vazias; só aparecem (em vermelho) - # quando um pacote de periférico passa por elas - sides = ( - (_WEST_ARROWS, self.pos_x != 0), - (_EAST_ARROWS, self.pos_x != self.mpsoc_x - 1), - (_SOUTH_ARROWS, self.pos_y != 0), - (_NORTH_ARROWS, self.pos_y != self.mpsoc_y - 1), - ) - - for side, has_neighbor in sides: - for widget_name, image in side.items(): - self._painter.set_image(getattr(self, widget_name), image if has_neighbor else None) + for arrow in self.arrows.values(): + arrow.set_painted(False) + if self.highlighted: + self.highlighted = False + self.update() def paint_arrow(self, arrow: int) -> None: - """Pinta de vermelho a seta indicada (constantes *_IN_*/*_OUT_* do MPSoCConfig).""" - if arrow not in _ARROWS: + """Pinta de vermelho a seta indicada (constantes *_IN_*/*_OUT_*/LOCAL_* do MPSoCConfig).""" + item = self.arrows.get(arrow) + if item is not None: + self.need_reset = True + item.set_painted(True) + + def set_highlight(self, on: bool = True) -> None: + """Moldura laranja em volta do roteador (some no próximo reset das setas).""" + self.highlighted = on + if on: + self.need_reset = True + self.update() + + def set_peripheral(self, side: str, name: str) -> None: + """Mostra o periférico `name` ligado à porta de borda `side` ("N", "S", "E" ou "W").""" + if self.peripherals.get(side) == name: return - - widget_name, image = _ARROWS[arrow] - self.need_reset = True - self._painter.set_image(getattr(self, widget_name), image) + self.peripherals[side] = name + if self._peripheral_added is not None: + self._peripheral_added(self, side, name) def update_throughput(self, port: int, value: float) -> None: """Atualiza o percentual de uso do enlace da porta física informada.""" - label_name = _PORT_LABELS.get(port) - if label_name is not None: - self.set_link_load(getattr(self, label_name), value) - - def set_link_load(self, direction_label: QWidget, value: float) -> None: - """ - Atualiza o percentual mostrado em um dos rótulos de enlace - (north_HIGH_Label, south_LOW_Label, etc.). - """ - value = min(value, 99.99) - direction_label.setText(f"{value:05.2f}%") - - -class RouterMatrixWidget(QWidget): + text = _format_load(value) + if port in _LOCAL_PORTS: + changed = self._local_load != text + self._local_load = text + elif port in _PORT_SIDES: + key = _PORT_SIDES[port] + changed = self._loads[key] != text + self._loads[key] = text + else: + return + if changed: + self.update() + + def link_load(self, port: int) -> str: + """Texto mostrado para a taxa de uso da porta (usado nos testes).""" + if port in _LOCAL_PORTS: + return self._local_load + return self._loads[_PORT_SIDES[port]] + + +# ========================================== +# VISTA DA MALHA +# ========================================== +class _ZoomControls(QFrame): + """Botões de zoom no canto inferior direito da vista.""" + + def __init__(self, view: "RouterMatrixWidget"): + super().__init__(view) + self.setObjectName("zoomControls") + self.setFrameShape(QFrame.Shape.StyledPanel) + self.setAutoFillBackground(True) + + layout = QHBoxLayout(self) + layout.setContentsMargins(3, 2, 3, 2) + layout.setSpacing(2) + + def button(text, tooltip, slot): + tool = QToolButton(self) + tool.setText(text) + tool.setToolTip(tooltip) + tool.setAutoRaise(True) + tool.clicked.connect(slot) + layout.addWidget(tool) + return tool + + button("−", "Zoom out (Ctrl+-)", view.zoom_out) + self.label = QLabel(self) + self.label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.label.setMinimumWidth(44) + self.label.setToolTip("Zoom level - Ctrl + mouse wheel to zoom") + layout.addWidget(self.label) + button("+", "Zoom in (Ctrl++)", view.zoom_in) + button("1:1", "Actual size (Ctrl+0)", view.reset_zoom) + button("Fit", "Fit the mesh to the window (double-click the background)", view.fit_to_view) + + def set_zoom(self, zoom: float) -> None: + self.label.setText(f"{round(zoom * 100)}%") + + +class RouterMatrixWidget(QGraphicsView): """ - Widget que organiza mpsoc_x * mpsoc_y roteadores em uma grade única, - desenhando uma borda a cada bloco de cluster_x por cluster_y roteadores - apenas para separação visual (sem alterar o total de roteadores). + Vista com os mpsoc_x * mpsoc_y roteadores, desenhando uma borda a cada + bloco de cluster_x por cluster_y roteadores apenas para separação visual + (sem alterar o total de roteadores). """ + zoom_changed = Signal(float) + def __init__(self, mpsoc_x: int, mpsoc_y: int, cluster_x: int, cluster_y: int, parent=None, mpsoc_config=None): - super().__init__(parent) + self._scene = _MeshScene() + super().__init__(self._scene, parent) + self._scene.setParent(self) self.mpsoc_config = mpsoc_config - - # Fundo e cores da grade seguem o tema (ver apply_theme) - self.setObjectName("routerMatrix") - self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) - - # mpsoc_x/mpsoc_y é o tamanho TOTAL da malha. self.total_cols = max(1, mpsoc_x) self.total_rows = max(1, mpsoc_y) - # cluster_x/cluster_y é o tamanho de cada bloco visual; se não - # definido (0) ou maior que a própria malha, cai para "sem cluster" - # (um bloco único, sem bordas internas). + # cluster_x/cluster_y é o tamanho de cada bloco visual; se não definido + # (0) ou maior que a própria malha, cai para "sem cluster" self.cluster_x = max(1, min(cluster_x, self.total_cols)) if cluster_x else self.total_cols self.cluster_y = max(1, min(cluster_y, self.total_rows)) if cluster_y else self.total_rows - # Guarda referência de cada roteador por posição global (x, y) - # e pelo endereço hamiltoniano usado no traffic_router.txt - self.routers: dict[tuple[int, int], RouterWidget] = {} - self.routers_by_address: dict[int, RouterWidget] = {} + # Roteadores por posição global (x, y) e pelo endereço hamiltoniano + # usado no traffic_router.txt + self.routers: dict[tuple[int, int], RouterItem] = {} + self.routers_by_address: dict[int, RouterItem] = {} + self.peripheral_items: dict[tuple[int, int, str], PeripheralItem] = {} + self._cluster_items: list[QGraphicsRectItem] = [] + self._channels = _channel_positions(mpsoc_config) + + self._zoom = 1.0 + # Até o usuário mexer no zoom, a malha acompanha o tamanho da janela + self._auto_fit = True + + self.setObjectName("routerMatrix") + self.setRenderHints(QPainter.RenderHint.Antialiasing | QPainter.RenderHint.TextAntialiasing) + self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag) + self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse) + self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter) + self.setViewportUpdateMode(QGraphicsView.ViewportUpdateMode.SmartViewportUpdate) + self.setFrameShape(QFrame.Shape.NoFrame) + self.viewport().grabGesture(Qt.GestureType.PinchGesture) self._build_matrix() - def _build_matrix(self) -> None: - n_clusters_x = math.ceil(self.total_cols / self.cluster_x) - n_clusters_y = math.ceil(self.total_rows / self.cluster_y) - - draw_borders = self.cluster_x < self.total_cols or self.cluster_y < self.total_rows - - outer_layout = QGridLayout(self) - # Roteadores grudados uns nos outros: só deixamos um pequeno - # espaçamento entre blocos quando existe borda de cluster separando - # eles; sem clusters, a malha fica totalmente colada (spacing 0). - outer_layout.setSpacing(2 if draw_borders else 0) - outer_layout.setContentsMargins(0, 0, 0, 0) - - # Clusters em coordenadas de "cluster" (cx, cy), com cy=0 embaixo - for cluster_y in range(n_clusters_y): - for cluster_x in range(n_clusters_x): - cluster_widget = self._build_cluster( - cluster_x, cluster_y, draw_borders - ) - - # Linha 0 do grid = topo da janela, então invertemos o Y - # para que o cluster (0,0) fique embaixo, como no print. - grid_row = n_clusters_y - 1 - cluster_y - outer_layout.addWidget(cluster_widget, grid_row, cluster_x) - - self.setLayout(outer_layout) - - def _build_cluster(self, cluster_x: int, cluster_y: int, - draw_borders: bool) -> QFrame: - """ - Cria um bloco visual contendo até cluster_x * cluster_y roteadores - (o último bloco de cada linha/coluna pode ter menos roteadores caso - a dimensão total não seja múltipla do tamanho do cluster). - """ - cluster_frame = QFrame() - cluster_frame.setObjectName(f"cluster_{cluster_x}x{cluster_y}") - - if draw_borders: - cluster_frame.setFrameShape(QFrame.Shape.Box) - cluster_frame.setFrameShadow(QFrame.Shadow.Plain) - cluster_frame.setLineWidth(1) - # A cor da borda vem da folha de estilo da matriz (_matrix_stylesheet) - cluster_frame.setProperty("clusterFrame", True) - - cluster_layout = QGridLayout(cluster_frame) - cluster_layout.setSpacing(0) - # Roteadores dentro do bloco ficam colados; só reservamos uma borda - # (via margem) quando o bloco realmente representa um cluster. - margin = 1 if draw_borders else 0 - cluster_layout.setContentsMargins(margin, margin, margin, margin) - - # Início (inclusive) e fim (exclusivo) deste bloco na malha total - x_start = cluster_x * self.cluster_x - x_end = min(x_start + self.cluster_x, self.total_cols) - y_start = cluster_y * self.cluster_y - y_end = min(y_start + self.cluster_y, self.total_rows) - - block_rows = y_end - y_start - - for global_y in range(y_start, y_end): - for global_x in range(x_start, x_end): - router = RouterWidget(global_x, global_y, cluster_frame, - self.total_cols, self.total_rows, self.mpsoc_config) - self.routers[(global_x, global_y)] = router - self.routers_by_address[router.router_address] = router + self._controls = _ZoomControls(self) + self._controls.set_zoom(self._zoom) - local_x = global_x - x_start - # Linha 0 do grid = topo, então invertemos o Y local também - local_row = block_rows - 1 - (global_y - y_start) - cluster_layout.addWidget(router, local_row, local_x) + for keys, slot in ((QKeySequence.StandardKey.ZoomIn, self.zoom_in), + (QKeySequence.StandardKey.ZoomOut, self.zoom_out), + ("Ctrl+=", self.zoom_in), + ("Ctrl+0", self.reset_zoom)): + shortcut = QShortcut(QKeySequence(keys), self) + shortcut.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut) + shortcut.activated.connect(slot) - cluster_frame.setLayout(cluster_layout) - return cluster_frame + # ---------- construção ---------- + def _body_origin(self, x: int, y: int) -> QPointF: + # Linha 0 da cena = topo: invertemos o Y para o 0x0 ficar embaixo + return QPointF(x * PITCH, (self.total_rows - 1 - y) * PITCH) - def get_router(self, x: int, y: int) -> RouterWidget | None: - """Retorna o RouterWidget na posição global (x, y), se existir.""" + def _build_matrix(self) -> None: + for y in range(self.total_rows): + for x in range(self.total_cols): + router = RouterItem(x, y, self.total_cols, self.total_rows, self.mpsoc_config) + router.setPos(self._body_origin(x, y)) + router._peripheral_added = self._add_peripheral + self._scene.addItem(router) + self.routers[(x, y)] = router + self.routers_by_address[router.router_address] = router + self._add_local_arrows(router) + + for (x, y), router in self.routers.items(): + for side in ("E", "N"): + neighbor = self.get_router(x + 1, y) if side == "E" else self.get_router(x, y + 1) + if neighbor is not None: + self._add_link(router, side, neighbor) + for side, at_border in (("W", x == 0), ("E", x == self.total_cols - 1), + ("S", y == 0), ("N", y == self.total_rows - 1)): + if at_border: + self._add_link(router, side, None) + + if self.cluster_x < self.total_cols or self.cluster_y < self.total_rows: + self._add_cluster_borders() + + self._update_scene_rect() + + def _edge_point(self, router: RouterItem, side: str, position: float, distance: float) -> QPointF: + """Ponto a `distance` para fora do lado `side`, a `position` ao longo dele.""" + origin = router.pos() + dx, dy = _OUTWARD[side] + if side in ("E", "W"): + edge_x = origin.x() + (BODY if side == "E" else 0) + return QPointF(edge_x + dx * distance, origin.y() + position) + edge_y = origin.y() + (BODY if side == "S" else 0) + return QPointF(origin.x() + position, edge_y + dy * distance) + + def _add_link(self, router: RouterItem, side: str, neighbor: RouterItem | None) -> None: + """ + Setas do lado `side` do roteador, uma por sentido e por canal. Com + `neighbor` None é uma borda da malha: as setas ligam o roteador ao + periférico e ficam ocultas até existir um (ou um pacote passar). + """ + near, far = ARROW_MARGIN, GAP - ARROW_MARGIN + # Desloca o par perpendicularmente: a saída fica "à esquerda" de quem sai + out_shift = -ARROW_OFFSET if side in ("E", "S") else ARROW_OFFSET + other = neighbor.label if neighbor is not None else f"{_SIDE_NAMES[side]} peripheral" + + for channel, position in self._channels.items(): + channel_name = "HIGH" if channel == HIGH else "LOW" + out_pos, in_pos = position + out_shift, position - out_shift + + outgoing = ArrowItem(self._edge_point(router, side, out_pos, near), + self._edge_point(router, side, out_pos, far), + optional=neighbor is None, + tooltip=f"{router.label} → {other} ({channel_name})") + incoming = ArrowItem(self._edge_point(router, side, in_pos, far), + self._edge_point(router, side, in_pos, near), + optional=neighbor is None, + tooltip=f"{other} → {router.label} ({channel_name})") + self._scene.addItem(outgoing) + self._scene.addItem(incoming) + + in_arrow, out_arrow = _SIDE_ARROWS[(side, channel)] + router.arrows[out_arrow] = outgoing + router.arrows[in_arrow] = incoming + if neighbor is not None: + neighbor_in, neighbor_out = _SIDE_ARROWS[(_OPPOSITE[side], channel)] + neighbor.arrows[neighbor_in] = outgoing + neighbor.arrows[neighbor_out] = incoming + + def _add_local_arrows(self, router: RouterItem) -> None: + """Setas diagonais da porta local, no canto superior esquerdo do corpo.""" + corner = router.pos() + shift = QPointF(5, -5) # perpendicular à diagonal + near, far = QPointF(-ARROW_MARGIN, -ARROW_MARGIN), QPointF(-GAP * 0.6, -GAP * 0.6) + + local_in = ArrowItem(corner + far + shift, corner + near + shift, + tooltip=f"PE → router {router.label} (local port)") + local_out = ArrowItem(corner + near - shift, corner + far - shift, + tooltip=f"Router {router.label} → PE (local port)") + self._scene.addItem(local_in) + self._scene.addItem(local_out) + router.arrows[MPSoCConfig.LOCAL_IN] = local_in + router.arrows[MPSoCConfig.LOCAL_OUT] = local_out + + def _add_peripheral(self, router: RouterItem, side: str, name: str) -> None: + key = (router.pos_x, router.pos_y, side) + old = self.peripheral_items.pop(key, None) + if old is not None: + self._scene.removeItem(old) + + size = PERIPHERAL_NS if side in ("N", "S") else PERIPHERAL_EW + center = self._edge_point(router, side, BODY / 2, GAP + (size.height() if side in ("N", "S") + else size.width()) / 2) + rect = QRectF(center.x() - size.width() / 2, center.y() - size.height() / 2, + size.width(), size.height()) + item = PeripheralItem(rect, name, router.label, side) + self._scene.addItem(item) + self.peripheral_items[key] = item + + for channel in self._channels: + for arrow in _SIDE_ARROWS[(side, channel)]: + router.arrows[arrow].set_optional(False) + + self._update_scene_rect() + + def _add_cluster_borders(self) -> None: + half = GAP / 2 + for cy in range(math.ceil(self.total_rows / self.cluster_y)): + for cx in range(math.ceil(self.total_cols / self.cluster_x)): + x0, y0 = cx * self.cluster_x, cy * self.cluster_y + x1 = min(x0 + self.cluster_x, self.total_cols) - 1 + y1 = min(y0 + self.cluster_y, self.total_rows) - 1 + top_left = self._body_origin(x0, y1) + bottom_right = self._body_origin(x1, y0) + QPointF(BODY, BODY) + rect = QRectF(top_left, bottom_right).adjusted(-half + 2, -half + 2, half - 2, half - 2) + + item = QGraphicsRectItem(rect) + item.setZValue(0) + item.setBrush(Qt.BrushStyle.NoBrush) + self._scene.addItem(item) + self._cluster_items.append(item) + self._apply_cluster_pen() + + def _apply_cluster_pen(self) -> None: + pen = QPen(QColor(self._scene.colors.cluster_border), 1.5, Qt.PenStyle.DashLine) + for item in self._cluster_items: + item.setPen(pen) + + def _update_scene_rect(self) -> None: + # Todas as setas de borda contam (mesmo ocultas), para a cena não mudar + # de tamanho quando um pacote de periférico aparece + rect = QRectF() + for item in self._scene.items(): + rect = rect.united(item.sceneBoundingRect()) + self._scene.setSceneRect(rect.adjusted(-16, -16, 16, 16)) + + # ---------- API usada pela janela principal e pela simulação ---------- + def get_router(self, x: int, y: int) -> RouterItem | None: + """Retorna o roteador na posição global (x, y), se existir.""" return self.routers.get((x, y)) - def apply_theme(self, dark: bool) -> None: - """Aplica as cores do tema claro ou escuro na grade.""" - self.setStyleSheet(_matrix_stylesheet(dark)) - - def get_router_by_address(self, router_address: int) -> RouterWidget | None: - """Retorna o RouterWidget pelo endereço hamiltoniano, se existir.""" + def get_router_by_address(self, router_address: int) -> RouterItem | None: + """Retorna o roteador pelo endereço hamiltoniano, se existir.""" return self.routers_by_address.get(router_address) + def set_peripherals(self, peripherals) -> None: + """Periféricos (testcase_files.Peripheral) nas bordas da malha.""" + for peripheral in peripherals: + router = self.get_router(peripheral.x, peripheral.y) + if router is not None: + router.set_peripheral(peripheral.side, peripheral.name) + def reset_routers(self) -> None: """Apaga as setas pintadas de todos os roteadores.""" for router in self.routers.values(): if router.need_reset: - router.reset_arrows() \ No newline at end of file + router.reset_arrows() + + def apply_theme(self, dark: bool) -> None: + """Aplica as cores do tema claro ou escuro na malha.""" + self._scene.colors = theme.matrix_colors(dark) + self.setBackgroundBrush(QBrush(QColor(self._scene.colors.canvas))) + self._apply_cluster_pen() + self._scene.update() + + def ensure_router_visible(self, router: RouterItem) -> None: + self.ensureVisible(router.sceneBoundingRect(), 20, 20) + + def to_image(self, scale: float = 2.0) -> QImage: + """A malha inteira (independente do zoom e da rolagem) como imagem.""" + source = self._scene.sceneRect() + image = QImage(max(1, round(source.width() * scale)), max(1, round(source.height() * scale)), + QImage.Format.Format_ARGB32) + image.fill(QColor(self._scene.colors.canvas)) + painter = QPainter(image) + painter.setRenderHints(QPainter.RenderHint.Antialiasing | QPainter.RenderHint.TextAntialiasing) + self._scene.render(painter, QRectF(image.rect()), source) + painter.end() + return image + + # ---------- zoom ---------- + @property + def zoom(self) -> float: + return self._zoom + + def set_zoom(self, zoom: float, user: bool = True) -> None: + zoom = max(ZOOM_MIN, min(ZOOM_MAX, zoom)) + if user: + self._auto_fit = False + if math.isclose(zoom, self._zoom): + return + self.scale(zoom / self._zoom, zoom / self._zoom) + self._zoom = zoom + self._controls.set_zoom(zoom) + self.zoom_changed.emit(zoom) + + def zoom_in(self) -> None: + self.set_zoom(self._zoom * ZOOM_STEP) + + def zoom_out(self) -> None: + self.set_zoom(self._zoom / ZOOM_STEP) + + def reset_zoom(self) -> None: + self.set_zoom(1.0) + + def fit_to_view(self, user: bool = True) -> None: + """Ajusta o zoom para a malha inteira caber na vista.""" + rect = self._scene.sceneRect() + viewport = self.viewport().size() + if rect.isEmpty() or viewport.isEmpty(): + return + zoom = min(viewport.width() / rect.width(), viewport.height() / rect.height()) + anchor = self.transformationAnchor() + self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter) + self.set_zoom(zoom, user) + self.setTransformationAnchor(anchor) + self.centerOn(rect.center()) + if user: + # "Fit" volta a acompanhar o tamanho da janela + self._auto_fit = True + + def _auto_zoom(self) -> None: + """Zoom inicial: 100%, ou menos se a malha não couber na janela.""" + if not self._auto_fit: + return + rect = self._scene.sceneRect() + viewport = self.viewport().size() + if rect.isEmpty() or viewport.isEmpty(): + return + fit = min(viewport.width() / rect.width(), viewport.height() / rect.height()) + if fit < 1: + self.fit_to_view(user=False) + else: + self.set_zoom(1.0, user=False) + self.centerOn(rect.center()) + + def wheelEvent(self, event): + if event.modifiers() & Qt.KeyboardModifier.ControlModifier: + steps = event.angleDelta().y() / 120 + if steps: + self.set_zoom(self._zoom * ZOOM_STEP ** steps) + event.accept() + return + super().wheelEvent(event) + + def viewportEvent(self, event): + if event.type() == QEvent.Type.Gesture: + pinch = event.gesture(Qt.GestureType.PinchGesture) + if pinch is not None: + self.set_zoom(self._zoom * pinch.scaleFactor()) + event.accept() + return True + elif event.type() == QEvent.Type.NativeGesture and \ + event.gestureType() == Qt.NativeGestureType.ZoomNativeGesture: + self.set_zoom(self._zoom * (1 + event.value())) + event.accept() + return True + return super().viewportEvent(event) + + def mouseDoubleClickEvent(self, event): + if self.itemAt(event.position().toPoint()) is None: + self.fit_to_view() + event.accept() + return + super().mouseDoubleClickEvent(event) + + def resizeEvent(self, event): + super().resizeEvent(event) + margin = 8 + viewport = self.viewport().geometry() + self._controls.adjustSize() + self._controls.move(viewport.right() - self._controls.width() - margin, + viewport.bottom() - self._controls.height() - margin) + self._controls.raise_() + # Adia para depois do layout terminar (o viewport já com o tamanho final) + QTimer.singleShot(0, self, self._auto_zoom) diff --git a/simulation.py b/simulation.py index 6a3fc41..0426772 100644 --- a/simulation.py +++ b/simulation.py @@ -20,6 +20,16 @@ # No "voltar no tempo", só os últimos pacotes são desenhados GO_REPAINT_LAST_PACKETS = 50 +# Acompanhando um traffic_router.txt que ainda está sendo gravado: intervalo +# entre as tentativas de ler novas linhas quando o arquivo chega ao fim +FOLLOW_POLL_MS = 250 + +# Lado da malha de onde vem o pacote que entra por cada porta +_PORT_SIDES = { + MPSoCConfig.NORTH0: "N", MPSoCConfig.NORTH1: "N", MPSoCConfig.SOUTH0: "S", MPSoCConfig.SOUTH1: "S", + MPSoCConfig.EAST0: "E", MPSoCConfig.EAST1: "E", MPSoCConfig.WEST0: "W", MPSoCConfig.WEST1: "W", +} + class CheckpointController: """ @@ -78,6 +88,8 @@ class SimulationController(QObject): finished = Signal() # Pacote com serviço que não existe no services.cfg unknown_service = Signal(object) + # Modo "follow": fim do arquivo alcançado, esperando novas linhas (True) ou voltaram a chegar (False) + waiting_for_data = Signal(bool) def __init__(self, mpsoc_config, mpsoc_information, router_matrix, parent=None): super().__init__(parent) @@ -103,6 +115,10 @@ def __init__(self, mpsoc_config, mpsoc_information, router_matrix, parent=None): self.timer = QTimer(self) self.timer.timeout.connect(self._play_step) + # Acompanhar um traffic_router.txt que ainda está sendo gravado + self.follow = False + self.waiting = False + # ========================================== # CONTROLES # ========================================== @@ -113,8 +129,25 @@ def set_speed(self, speed): self.timer.setInterval(self._interval()) def _interval(self): + if self.waiting: + return FOLLOW_POLL_MS return max(0, 400 - self.speed * 4) + def set_follow(self, follow): + """Em vez de parar no fim do arquivo, espera o simulador gravar mais pacotes.""" + self.follow = follow + self.mpsoc_information.read_traffic.follow = follow + if not follow: + self._set_waiting(False) + + def _set_waiting(self, waiting): + if waiting == self.waiting: + return + self.waiting = waiting + if self.running: + self.timer.setInterval(self._interval()) + self.waiting_for_data.emit(waiting) + def play(self): if self.running: return @@ -182,10 +215,15 @@ def next_packet(self, repaint=True, limit_time=0): packet = self.mpsoc_information.get_next_packet(self.packet_filter, limit_time) if packet is None: + if self.follow: + # O simulador ainda pode gravar mais pacotes: continua rodando + self._set_waiting(True) + return -1 self.router_matrix.reset_routers() self.stop() self.finished.emit() return -1 + self._set_waiting(False) if packet.service not in self.mpsoc_config.services_hash: self.stop() @@ -202,16 +240,31 @@ def next_packet(self, repaint=True, limit_time=0): def repaint_router(self, packet): """Pinta o caminho do pacote: seta de entrada no roteador e de saída no vizinho.""" - router = self.router_matrix.get_router_by_address(packet.router_address) + if packet.input_port in (MPSoCConfig.LOCAL0, MPSoCConfig.LOCAL1): + # Pacote novo saindo do PE: apaga o caminho anterior se nenhum + # outro pacote estiver em trânsito + if not self.unfinished_packets: + self.router_matrix.reset_routers() + + # Destinos fora da malha (ex.: periféricos) nunca "chegam", então não são aguardados + if 0 <= packet.target_router < self.mpsoc_config.get_pe_number(): + self.unfinished_packets.append(packet.target_router) + + self.paint_hop(packet.router_address, packet.input_port, packet.target_router) + + # Pacote chegou ao destino + if packet.router_address == packet.target_router and packet.router_address in self.unfinished_packets: + self.unfinished_packets.remove(packet.router_address) + + def paint_hop(self, address, port, target): + """Seta de entrada no roteador (e de saída no vizinho de onde o pacote veio).""" + router = self.router_matrix.get_router_by_address(address) if router is None: return - port = packet.input_port - neighbors = self.mpsoc_information.neighbors - address = packet.router_address - if port in _INPUT_ARROWS: local_arrow, neighbor_arrow = _INPUT_ARROWS[port] + neighbors = self.mpsoc_information.neighbors if port in (MPSoCConfig.NORTH0, MPSoCConfig.NORTH1): neighbor_address = neighbors.get_vizinho_cima(address) @@ -225,28 +278,35 @@ def repaint_router(self, packet): neighbor = self.router_matrix.get_router_by_address(neighbor_address) if neighbor is not None: neighbor.paint_arrow(neighbor_arrow) + elif neighbor_address == -1 and port in _PORT_SIDES: + # Veio de fora da malha: marca o periférico, se o YAML não o declarou + side = _PORT_SIDES[port] + if side not in router.peripherals: + router.set_peripheral(side, "Peripheral") elif port in (MPSoCConfig.LOCAL0, MPSoCConfig.LOCAL1): - # Pacote novo saindo do PE: apaga o caminho anterior se nenhum - # outro pacote estiver em trânsito - if not self.unfinished_packets: - self.router_matrix.reset_routers() - local_arrow = MPSoCConfig.LOCAL_IN - - # Destinos fora da malha (ex.: periféricos) nunca "chegam", então não são aguardados - if 0 <= packet.target_router < self.mpsoc_config.get_pe_number(): - self.unfinished_packets.append(packet.target_router) else: return router.paint_arrow(local_arrow) - - # Pacote chegou ao destino - if address == packet.target_router: + if address == target: router.paint_arrow(MPSoCConfig.LOCAL_OUT) - if address in self.unfinished_packets: - self.unfinished_packets.remove(address) + + def show_path(self, hops, target, highlight=None): + """ + Mostra na malha o caminho de um pacote já analisado: `hops` são + (roteador, porta de entrada) na ordem; `highlight` recebe a moldura. + Para a simulação para o desenho não ser apagado pelo próximo pacote. + """ + self.stop() + self.reset_graphical_path() + for address, port in hops: + self.paint_hop(address, port, target) + if highlight is not None: + router = self.router_matrix.get_router_by_address(highlight) + if router is not None: + router.set_highlight(True) # ========================================== # TAXA DE USO DOS ENLACES diff --git a/statistics_windows.py b/statistics_windows.py new file mode 100644 index 0000000..278dab9 --- /dev/null +++ b/statistics_windows.py @@ -0,0 +1,828 @@ +""" +Janelas "Traffic Statistics" e "Application Timeline" (menu Tools). + +Não existem no GraphicalDebugger em Java: implementam métricas usadas no +artigo do Memphis (Ruaro et al., DAES 2019) — vazão em Mbps, taxa de +injeção e latência (Seção 6.2/6.3) e a admissão de aplicações (Seção 3.4). +A lógica está em analysis.py. +""" + +import os + +from PySide6.QtCore import QAbstractTableModel, QModelIndex, QRect, QSortFilterProxyModel, Qt +from PySide6.QtGui import QColor, QFont, QPainter, QPen +from PySide6.QtWidgets import (QAbstractItemView, QComboBox, QHBoxLayout, QHeaderView, QLabel, QPushButton, + QScrollArea, QSpinBox, QSplitter, QTableView, QTableWidget, QTableWidgetItem, + QTabWidget, QToolTip, QVBoxLayout, QWidget) + +import analysis +import export +import path_view +from overview_windows import _LiveOverview, _router_label +from util.MPSoCConfig import MPSoCConfig + + +def _ticks_to_us(mpsoc_config, ticks): + return ticks * mpsoc_config.clock_period_in_ns / 1000.0 + + +class _NumericItem(QTableWidgetItem): + """Célula que ordena pelo valor numérico (UserRole), não pelo texto.""" + + def __init__(self, text, value): + super().__init__(text) + self.setData(Qt.ItemDataRole.UserRole, value) + self.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) + + def __lt__(self, other): + return self.data(Qt.ItemDataRole.UserRole) < other.data(Qt.ItemDataRole.UserRole) + + +def _row_id(table, row): + item = table.item(row, 0) + return None if item is None else item.data(ROW_ID_ROLE) + + +def _sortable_table(headers): + table = QTableWidget(0, len(headers)) + table.setHorizontalHeaderLabels(headers) + table.verticalHeader().setVisible(False) + table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + table.horizontalHeader().setStretchLastSection(True) + # O QTableWidget começa com o indicador em ordem decrescente + table.horizontalHeader().setSortIndicator(0, Qt.SortOrder.AscendingOrder) + return table + + +# Guarda, na primeira coluna, o índice da linha na lista original (a ordenação muda as linhas) +ROW_ID_ROLE = Qt.ItemDataRole.UserRole + 1 + + +def _fill_table(table, rows, row_ids=None): + """ + rows: listas de (texto, valor numérico ou None). row_ids (padrão: o + índice da linha em `rows`) fica no ROW_ID_ROLE da primeira coluna. + """ + table.setSortingEnabled(False) + table.setRowCount(len(rows)) + for row, cells in enumerate(rows): + for column, (text, value) in enumerate(cells): + item = QTableWidgetItem(text) if value is None else _NumericItem(text, value) + if column == 0: + item.setData(ROW_ID_ROLE, row if row_ids is None else row_ids[row]) + table.setItem(row, column, item) + # Reordena pela coluna escolhida pelo usuário + table.setSortingEnabled(True) + + +# ========================================== +# TRAFFIC STATISTICS +# ========================================== +class _MessageModel(QAbstractTableModel): + HEADERS = ("Injected (tick)", "Source", "Target", "Service", "Flits", "Hops", "Latency (cycles)", "Application") + + def __init__(self, mpsoc_config): + super().__init__() + self.mpsoc_config = mpsoc_config + self.messages = [] + + def set_messages(self, messages): + self.beginResetModel() + self.messages = messages + self.endResetModel() + + def rowCount(self, parent=QModelIndex()): + return 0 if parent.isValid() else len(self.messages) + + def columnCount(self, parent=QModelIndex()): + return len(self.HEADERS) + + def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole): + if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: + return self.HEADERS[section] + return None + + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + message = self.messages[index.row()] + column = index.column() + config = self.mpsoc_config + + if role == Qt.ItemDataRole.UserRole: # valor usado para ordenar + return (message.injected, message.source, message.target, message.service, message.flits, + message.hops, message.latency, min(message.apps, default=-1))[column] + if role == Qt.ItemDataRole.TextAlignmentRole and column in (0, 4, 5, 6): + return Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter + if role != Qt.ItemDataRole.DisplayRole: + return None + + if column == 0: + return str(message.injected) + if column == 1: + label = _router_label(config, message.source) + return f"Periph. @ {label}" if message.from_peripheral else label + if column == 2: + return _router_label(config, message.target) + if column == 3: + return config.get_string_service_name(message.service) + if column == 4: + return str(message.flits) + if column == 5: + return str(message.hops) if message.delivered >= 0 else "-" + if column == 6: + return str(message.latency) if message.delivered >= 0 else "not delivered" + return ", ".join(analysis.app_label(config, app) for app in sorted(message.apps)) or "kernel" + + +class _MessageFilter(QSortFilterProxyModel): + def __init__(self): + super().__init__() + self.app = None + self.setSortRole(Qt.ItemDataRole.UserRole) + + def set_app(self, app): + if hasattr(self, "beginFilterChange"): # Qt 6.10+ + self.beginFilterChange() + self.app = app + self.endFilterChange() + else: + self.app = app + self.invalidateFilter() + + def filterAcceptsRow(self, row, parent): + message = self.sourceModel().messages[row] + if self.app is not None and self.app not in message.apps: + return False + return True + + +class TrafficStatisticsWindow(_LiveOverview): + """ + Vazão por enlace (Mbps e % da capacidade), métricas por aplicação, a + latência de cada mensagem, a validação do roteamento XY e os pacotes + parados (possível deadlock), até o tempo atual da simulação. + """ + + # Sem avançar por este número de ciclos, o pacote é considerado parado + DEFAULT_BLOCKED_THRESHOLD = 10000 + + def __init__(self, mpsoc_config, mpsoc_information, simulation=None): + self.tracker = analysis.TrafficTracker(mpsoc_config) + super().__init__(mpsoc_config, mpsoc_information, simulation) + self.setWindowTitle("Traffic Statistics") + self.resize(820, 560) + + self.summary = QLabel() + self.summary.setWordWrap(True) + self.summary.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + + self.links_table = _sortable_table(["Router", "Input port", "Volume (flits)", + "Throughput (Mbps)", "Link usage (%)"]) + self.links_table.setToolTip("Average since the beginning of the simulation, per router input port.\n" + "Follows the Service/PE/Application filter.") + + self.apps_table = _sortable_table(["Application", "Messages", "Flits", "Throughput (Mbps)", + "Injection rate (%)", "Avg latency", "Median latency", + "Max latency", "Avg hops"]) + self.apps_table.setToolTip("Throughput = flits of the delivered messages of the application over the\n" + "interval between its first injection and its last delivery.\n" + "Injection rate = throughput / link capacity. Latencies in clock cycles.") + + self.message_model = _MessageModel(mpsoc_config) + self.message_filter = _MessageFilter() + self.message_filter.setSourceModel(self.message_model) + self.messages_view = QTableView() + self.messages_view.setModel(self.message_filter) + self.messages_view.setSortingEnabled(True) + self.messages_view.sortByColumn(0, Qt.SortOrder.AscendingOrder) + self.messages_view.verticalHeader().setVisible(False) + self.messages_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.messages_view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + self.messages_view.horizontalHeader().setStretchLastSection(True) + self.messages_view.doubleClicked.connect(self._show_message_path) + self.messages_view.setToolTip("Double click a packet to show its path on the main window mesh") + self.message_app_combo = QComboBox() + self.message_app_combo.currentIndexChanged.connect( + lambda _index: self.message_filter.set_app(self.message_app_combo.currentData())) + messages_tab = QWidget() + messages_layout = QVBoxLayout(messages_tab) + app_row = QHBoxLayout() + app_row.addWidget(QLabel("Application:")) + app_row.addWidget(self.message_app_combo, 1) + messages_layout.addLayout(app_row) + messages_layout.addWidget(self.messages_view) + latency_note = QLabel("Latency = from the header entering the NoC (local port or peripheral) until the " + "last flit leaves the target router (network only, without the kernel time). " + "traffic_router.txt has no packet ID, so each packet is followed hop by hop by " + "service, size, neighbor router and time.") + latency_note.setWordWrap(True) + messages_layout.addWidget(latency_note) + + routing_tab = self._build_routing_tab() + blocked_tab = self._build_blocked_tab() + + self.tabs = QTabWidget() + self.tabs.addTab(self.links_table, "Links") + self.tabs.addTab(self.apps_table, "Applications") + self.tabs.addTab(messages_tab, "Messages") + self.tabs.addTab(routing_tab, "Routing") + self.tabs.addTab(blocked_tab, "Blocked") + self.tabs.currentChanged.connect(lambda _index: self.refresh()) + + # Tabela exportada por aba (mesma ordem das abas) + self.tab_tables = [(self.links_table, "links"), (self.apps_table, "applications"), + (self.messages_view, "messages"), (self.violations_table, "routing_violations"), + (self.blocked_table, "blocked_packets")] + for table, name in self.tab_tables: + export.install_export_actions(table, name, csv_view=table) + + csv_button = QPushButton("Export CSV…") + csv_button.setToolTip("Save the table of the current tab (as filtered and sorted)") + csv_button.clicked.connect(self._export_current_table) + png_button = QPushButton("Save PNG…") + png_button.clicked.connect(lambda: export.export_png(self, self, "traffic_statistics")) + buttons = QHBoxLayout() + buttons.addStretch() + buttons.addWidget(csv_button) + buttons.addWidget(png_button) + + layout = QVBoxLayout(self) + layout.addWidget(self.summary) + layout.addWidget(self.tabs) + layout.addLayout(buttons) + + self._fill_app_combo() + self.refresh() + + def _build_routing_tab(self): + tab = QWidget() + self.routing_summary = QLabel() + self.routing_summary.setWordWrap(True) + self.violations_table = _sortable_table(["Time (tick)", "Router", "Input port", "Target", "Service", + "Moving", "Tasks"]) + self.violations_table.setToolTip("Hop records whose input port is not compatible with XY routing\n" + "(X first, then Y) towards the target in the header.\n" + "Double click to show the packet path on the main window mesh.") + self.violations_table.cellDoubleClicked.connect(self._show_violation_path) + self._violation_rows = [] + layout = QVBoxLayout(tab) + layout.addWidget(self.routing_summary) + layout.addWidget(self.violations_table) + return tab + + def _build_blocked_tab(self): + tab = QWidget() + self.blocked_threshold = QSpinBox() + self.blocked_threshold.setRange(1, 1_000_000_000) + self.blocked_threshold.setSingleStep(1000) + self.blocked_threshold.setValue(self.DEFAULT_BLOCKED_THRESHOLD) + self.blocked_threshold.setSuffix(" cycles") + self.blocked_threshold.valueChanged.connect(lambda _value: self.refresh()) + threshold_row = QHBoxLayout() + threshold_row.addWidget(QLabel("Packets in flight without progress for at least")) + threshold_row.addWidget(self.blocked_threshold) + threshold_row.addStretch() + + self.deadlock_label = QLabel() + self.deadlock_label.setWordWrap(True) + self.blocked_table = _sortable_table(["Injected (tick)", "Last seen (tick)", "Stopped for (cycles)", + "Source", "Target", "Service", "Last router", "Hops seen", + "Next XY hop", "Status"]) + self.blocked_table.setToolTip("Double click to show where the packet stopped on the main window mesh") + self.blocked_table.cellDoubleClicked.connect(self._show_blocked_path) + self._blocked_rows = [] + note = QLabel("A packet with no record after its injection may also be a gap in traffic_router.txt. " + "A circular wait (each packet waiting for the link held by the next one) indicates a " + "deadlock, which XY routing should never produce.") + note.setWordWrap(True) + layout = QVBoxLayout(tab) + layout.addLayout(threshold_row) + layout.addWidget(self.deadlock_label) + layout.addWidget(self.blocked_table) + layout.addWidget(note) + return tab + + def _show_message_path(self, index): + message = self.message_model.messages[self.message_filter.mapToSource(index).row()] + service = self.mpsoc_config.get_string_service_name(message.service) + path_view.show_message(message, description=f"Path of {service} injected at {message.injected}") + + def _show_violation_path(self, row, _column): + index = _row_id(self.violations_table, row) + if index is None or index >= len(self._violation_rows): + return + violation = self._violation_rows[index] + description = f"XY violation at {_router_label(self.mpsoc_config, violation.router)}, tick {violation.time}" + if violation.message is not None: + path_view.show_message(violation.message, highlight=violation.router, description=description) + else: + path_view.show_record(violation.router, violation.port, violation.target, description) + + def _show_blocked_path(self, row, _column): + index = _row_id(self.blocked_table, row) + if index is not None and index < len(self._blocked_rows): + message = self._blocked_rows[index] + path_view.show_message(message, description=f"Packet stopped at " + f"{_router_label(self.mpsoc_config, message.last_router)}") + + def _export_current_table(self): + table, name = self.tab_tables[self.tabs.currentIndex()] + export.export_csv(self, table, name) + + def _fill_app_combo(self): + self.message_app_combo.blockSignals(True) + self.message_app_combo.clear() + self.message_app_combo.addItem("All", None) + for app in sorted(analysis.known_app_ids(self.mpsoc_config)): + self.message_app_combo.addItem(analysis.app_label(self.mpsoc_config, app), app) + self.message_app_combo.blockSignals(False) + self.message_filter.set_app(None) + + def rebuild(self): + self.tracker = analysis.TrafficTracker(self.mpsoc_config) + self.message_model.mpsoc_config = self.mpsoc_config + self._fill_app_combo() + self.refresh() + + # ------------------------------------------ + def _update_tracker(self): + """Analisa os pacotes já processados pela simulação (recomeça ao voltar no tempo).""" + reader = self.mpsoc_information.read_traffic + consumed = reader.packet_read_control + if consumed < self.tracker.consumed: + self.tracker.reset() + for packet in reader.all_packets[self.tracker.consumed:consumed]: + self.tracker.consume(packet) + return reader.all_packets[consumed - 1].time if consumed else 0 + + def refresh(self): + self._dirty = False + if not hasattr(self, "tabs"): + return # chamado pela base antes da interface existir + + config = self.mpsoc_config + now = self._update_tracker() + capacity = analysis.link_capacity_mbps(config) + delivered = self.tracker.delivered() + + frequency = 1000.0 / config.clock_period_in_ns if config.clock_period_in_ns else 0 + self.summary.setText( + f"Simulated time: {now} ticks ({_ticks_to_us(config, now) / 1000:.5f} ms)   " + f"Link capacity: {capacity:.0f} Mbps ({config.flit_size}-bit flits @ {frequency:.0f} MHz)
" + f"Messages: {len(self.tracker.messages)} injected, {len(delivered)} delivered, " + f"{self.tracker.pending_count()} in flight, {self.tracker.lost} without recorded delivery") + + current = self.tabs.currentIndex() + if current == 0: + self._update_links(now, capacity) + elif current == 1: + self._update_apps(delivered, capacity) + elif current == 2: + self.message_model.set_messages(list(self.tracker.messages)) + elif current == 3: + self._update_routing() + else: + self._update_blocked(now) + + def _update_routing(self): + config = self.mpsoc_config + tracker = self.tracker + non_xy, anomalies = analysis.routing_summary(tracker) + checked = sum(tracker.hop_records.values()) + excluded = sum(total for _count, total in non_xy.values()) + + if anomalies: + status = (f"⚠ {len(anomalies)} hop record(s) outside XY routing " + f"in {checked - excluded} checked records.") + else: + status = f"✓ No XY routing violation in {checked - excluded} checked hop records." + if non_xy: + services = ", ".join(f"{config.get_string_service_name(service)} ({count}/{total})" + for service, (count, total) in sorted(non_xy.items())) + status += (f"
Not checked — services that do not follow XY at all, most likely broadcast " + f"(e.g. the Memphis-V management network on the LOW channel): {services}.") + self.routing_summary.setText(status) + + rows = [] + self._violation_rows = anomalies + for violation in anomalies: + # Lado de onde o pacote veio -> sentido em que ele andava + side = analysis._PORT_SIDE.get(violation.port, (0, 0)) + direction = {(-1, 0): "east", (1, 0): "west", (0, -1): "north", (0, 1): "south"}.get(side, "-") + tasks = f"{violation.task_source} → {violation.task_target}" if violation.task_source >= 0 else "-" + rows.append([(str(violation.time), violation.time), + (_router_label(config, violation.router), None), + (MPSoCConfig.get_port_string(violation.port), None), + (_router_label(config, violation.target), None), + (config.get_string_service_name(violation.service), None), + (direction, None), (tasks, None)]) + _fill_table(self.violations_table, rows) + + def _update_blocked(self, now): + config = self.mpsoc_config + blocked = analysis.blocked_messages(self.tracker, now, self.blocked_threshold.value()) + cycles = analysis.find_wait_cycles(config, blocked) + in_cycle = {id(message) for cycle in cycles for message in cycle} + + if cycles: + descriptions = [" → ".join(f"{_router_label(config, m.last_router)} " + f"({config.get_string_service_name(m.service)})" for m in cycle) + for cycle in cycles] + self.deadlock_label.setText("⚠ Circular wait found (possible deadlock):" + "
" + "
".join(descriptions)) + elif blocked: + self.deadlock_label.setText(f"{len(blocked)} packet(s) stopped. " + "No circular wait among them.") + else: + self.deadlock_label.setText("✓ No packet stopped for this long.") + + rows = [] + self._blocked_rows = blocked + for message in blocked: + following = analysis.next_hop(config, message.last_router, message.target) + expected = analysis.hops(config, message.source, message.target) + if id(message) in in_cycle: + status = "in circular wait" + elif message.hops_seen == 0: + status = "no record after injection" + else: + status = "stopped mid-path" + stopped = now - message.last_time + rows.append([(str(message.injected), message.injected), + (str(message.last_time), message.last_time), + (str(stopped), stopped), + (_router_label(config, message.source), None), + (_router_label(config, message.target), None), + (config.get_string_service_name(message.service), None), + (_router_label(config, message.last_router), None), + (f"{message.hops_seen}/{expected}", message.hops_seen), + (_router_label(config, following) if following is not None else "local", None), + (status, None)]) + _fill_table(self.blocked_table, rows) + + def _update_links(self, now, capacity): + config = self.mpsoc_config + rows = [] + for address in range(config.get_pe_number()): + router = self.mpsoc_information.get_router_information(address) + for port in range(MPSoCConfig.NPORT): + flits = router.get_port_total_volume_in_flits(port) + if not flits: + continue + throughput = analysis.mbps(config, flits, now) + usage = throughput * 100.0 / capacity if capacity else 0.0 + rows.append([(_router_label(config, address), None), + (MPSoCConfig.get_port_string(port), None), + (str(flits), flits), + (f"{throughput:.2f}", throughput), + (f"{usage:.3f}", usage)]) + _fill_table(self.links_table, rows) + + def _update_apps(self, delivered, capacity): + config = self.mpsoc_config + rows = [] + for app, stats in sorted(analysis.traffic_by_app(delivered).items()): + throughput = analysis.mbps(config, stats.flits, stats.active_cycles) + rate = throughput * 100.0 / capacity if capacity else 0.0 + rows.append([(analysis.app_label(config, app), None), + (str(stats.messages), stats.messages), + (str(stats.flits), stats.flits), + (f"{throughput:.2f}", throughput), + (f"{rate:.3f}", rate), + (f"{stats.average_latency:.1f}", stats.average_latency), + (f"{stats.median_latency:g}", stats.median_latency), + (str(stats.latency_max), stats.latency_max), + (f"{stats.average_hops:.2f}", stats.average_hops)]) + _fill_table(self.apps_table, rows) + + +# ========================================== +# APPLICATION TIMELINE +# ========================================== +_PHASES = ( + # (nome, cor, início, fim) + ("Mapping", QColor(230, 159, 0), lambda t: t.received, lambda t: t.mapped), + ("Task allocation / release", QColor(170, 90, 200), lambda t: t.mapped if t.mapped >= 0 else t.received, + lambda t: t.start), + ("Execution", QColor(0, 114, 178), lambda t: t.start, lambda t: t.end), +) +_ALLOCATION_COLOR = QColor(0, 158, 115) +_TERMINATION_COLOR = QColor(213, 94, 0) + + +class TimelineChart(QWidget): + """Uma linha por aplicação com as fases da admissão e da execução.""" + + ROW_HEIGHT = 34 + TOP = 34 + LEFT = 170 + RIGHT = 20 + + def __init__(self, mpsoc_config, parent=None): + super().__init__(parent) + self.mpsoc_config = mpsoc_config + self.timelines = [] + self.range = (0, 1) + self.current_time = -1 + self.setMouseTracking(True) + + def set_timelines(self, timelines, time_range): + self.timelines = timelines + self.set_range(*time_range) + self.setMinimumHeight(self.TOP + len(timelines) * self.ROW_HEIGHT + 60) + + def set_range(self, start, end): + self.range = (start, max(end, start + 1)) + self.update() + + def set_current_time(self, ticks): + self.current_time = ticks + self.update() + + def _x(self, ticks): + start, end = self.range + width = max(1, self.width() - self.LEFT - self.RIGHT) + return self.LEFT + (ticks - start) * width / (end - start) + + def _row_rect(self, row): + return QRect(self.LEFT, self.TOP + row * self.ROW_HEIGHT + 8, self.width() - self.LEFT - self.RIGHT, + self.ROW_HEIGHT - 16) + + def _phase_rects(self, row, timeline): + rect = self._row_rect(row) + for name, color, begin, end in _PHASES: + start, finish = begin(timeline), end(timeline) + if start < 0 or finish < 0 or finish < start: + continue + x1, x2 = self._x(start), self._x(finish) + yield name, color, start, finish, QRect(int(x1), rect.top(), max(2, int(x2 - x1)), rect.height()) + + def paintEvent(self, event): + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + palette = self.palette() + painter.fillRect(self.rect(), palette.base().color()) + text = palette.text().color() + grid = QColor(text) + grid.setAlpha(60) + + if not self.timelines: + painter.setPen(text) + painter.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, "No application found in the trace") + return + + bottom = self.TOP + len(self.timelines) * self.ROW_HEIGHT + + # Régua de tempo + start, end = self.range + painter.setPen(text) + step = self._ruler_step(end - start) + tick = (start // step + 1) * step + while tick < end: + x = int(self._x(tick)) + painter.setPen(grid) + painter.drawLine(x, self.TOP, x, bottom) + painter.setPen(text) + painter.drawText(QRect(x - 60, bottom + 4, 120, 16), Qt.AlignmentFlag.AlignHCenter, + self._format_ticks(tick)) + tick += step + painter.drawText(QRect(self.LEFT, bottom + 22, self.width() - self.LEFT - self.RIGHT, 16), + Qt.AlignmentFlag.AlignHCenter, "time") + + # Legenda + x = self.LEFT + for name, color, _begin, _end in _PHASES: + painter.fillRect(QRect(x, 10, 12, 12), color) + painter.drawText(x + 16, 21, name) + x += 26 + painter.fontMetrics().horizontalAdvance(name) + painter.setPen(QPen(_ALLOCATION_COLOR, 2)) + painter.drawLine(x, 10, x, 22) + painter.setPen(text) + painter.drawText(x + 6, 21, "task code arrived") + x += 20 + painter.fontMetrics().horizontalAdvance("task code arrived") + painter.setPen(QPen(_TERMINATION_COLOR, 2)) + painter.drawLine(x, 10, x, 22) + painter.setPen(text) + painter.drawText(x + 6, 21, "task terminated") + + for row, timeline in enumerate(self.timelines): + rect = self._row_rect(row) + painter.setPen(text) + label = analysis.app_label(self.mpsoc_config, timeline.app) + painter.drawText(QRect(6, rect.top(), self.LEFT - 12, rect.height()), + Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft, label) + painter.setPen(grid) + painter.drawLine(self.LEFT, rect.bottom() + 8, self.width() - self.RIGHT, rect.bottom() + 8) + + painter.setClipRect(QRect(self.LEFT, 0, self.width() - self.LEFT - self.RIGHT, self.height())) + for _name, color, _start, _finish, phase_rect in self._phase_rects(row, timeline): + painter.fillRect(phase_rect, color) + for time, _pe in timeline.task_allocated.values(): + x = int(self._x(time)) + painter.setPen(QPen(_ALLOCATION_COLOR, 2)) + painter.drawLine(x, rect.top() - 5, x, rect.top() + 4) + for time in timeline.task_terminated.values(): + x = int(self._x(time)) + painter.setPen(QPen(_TERMINATION_COLOR, 2)) + painter.drawLine(x, rect.bottom() - 4, x, rect.bottom() + 5) + painter.setClipping(False) + + if self.current_time >= start and self.current_time <= end: + x = int(self._x(self.current_time)) + painter.setPen(QPen(QColor(Qt.GlobalColor.red), 1, Qt.PenStyle.DashLine)) + painter.drawLine(x, self.TOP, x, bottom) + + def _format_ticks(self, ticks): + us = _ticks_to_us(self.mpsoc_config, ticks) + return f"{us / 1000:g} ms" if us >= 1000 else f"{us:g} µs" + + def _ruler_step(self, span): + """Passo 1-2-5 em ticks que dá ~8 marcas na régua.""" + target = max(1, span / 8) + power = 1 + while power * 10 <= target: + power *= 10 + for factor in (1, 2, 5, 10): + if power * factor >= target: + return power * factor + return power * 10 + + def mouseMoveEvent(self, event): + point = event.position().toPoint() + for row, timeline in enumerate(self.timelines): + for name, _color, start, finish, rect in self._phase_rects(row, timeline): + if rect.adjusted(-2, 0, 2, 0).contains(point): + cycles = finish - start + QToolTip.showText(event.globalPosition().toPoint(), + f"{analysis.app_label(self.mpsoc_config, timeline.app)}\n{name}\n" + f"{start} → {finish} ticks\n{cycles} cycles " + f"({_ticks_to_us(self.mpsoc_config, cycles):.2f} µs)", self) + return + QToolTip.hideText() + + def wheelEvent(self, event): + """Ctrl + roda: zoom em torno do mouse.""" + if not event.modifiers() & Qt.KeyboardModifier.ControlModifier: + event.ignore() + return + start, end = self.range + width = max(1, self.width() - self.LEFT - self.RIGHT) + anchor = start + (event.position().x() - self.LEFT) * (end - start) / width + factor = 0.8 if event.angleDelta().y() > 0 else 1.25 + new_start = anchor - (anchor - start) * factor + new_end = anchor + (end - anchor) * factor + self.set_range(max(0, int(new_start)), int(new_end)) + event.accept() + + +class ApplicationTimelineWindow(QWidget): + """ + Linha do tempo da admissão de cada aplicação (protocolo da Seção 3.4 do + artigo): pedido recebido pelo mapper, mapeamento, chegada do código de + cada tarefa, liberação (TASK_RELEASE) e execução até o término. + + Analisa o traffic_router.txt inteiro (não só até o tempo atual) e o log + do mapper_task (log/logx.txt do cenário). + """ + + def __init__(self, mpsoc_config, simulation=None, parent=None): + super().__init__(parent, Qt.WindowType.Window) + self.mpsoc_config = mpsoc_config + self.simulation = None + self.timelines = [] + self.setWindowTitle("Application Timeline") + self.resize(1000, 620) + + self.chart = TimelineChart(mpsoc_config) + self.zoom_combo = QComboBox() + self.zoom_combo.addItems(["Whole run", "Admission only"]) + self.zoom_combo.currentIndexChanged.connect(self._apply_zoom) + reload_button = QPushButton("Reload") + reload_button.clicked.connect(self.load) + csv_button = QPushButton("Export CSV…") + csv_button.clicked.connect(lambda: export.export_csv(self, self.table, "application_timeline")) + png_button = QPushButton("Save PNG…") + png_button.setToolTip("Save the chart as it is shown (with the current zoom)") + png_button.clicked.connect(lambda: export.export_png(self, self.chart, "application_timeline")) + self.source_label = QLabel() + self.source_label.setWordWrap(True) + + controls = QHBoxLayout() + controls.addWidget(QLabel("Zoom:")) + controls.addWidget(self.zoom_combo) + controls.addWidget(QLabel("(Ctrl + mouse wheel zooms around the cursor)")) + controls.addStretch() + controls.addWidget(csv_button) + controls.addWidget(png_button) + controls.addWidget(reload_button) + + chart_scroll = QScrollArea() + chart_scroll.setWidgetResizable(True) + chart_scroll.setWidget(self.chart) + + self.table = _sortable_table(["Application", "Tasks", "Request (tick)", "Mapped (tick)", + "Released (tick)", "Terminated (tick)", "Mapping (cycles)", + "Admission (cycles)", "Admission (µs)", "Execution (ms)"]) + self.table.setToolTip("Admission = from the request received by the mapper until TASK_RELEASE.\n" + "Double click a row to zoom the chart on that application's admission.") + self.table.cellDoubleClicked.connect(self._zoom_on_row) + export.install_export_actions(self.table, "application_timeline", csv_view=self.table) + export.install_export_actions(self.chart, "application_timeline", png_widget=self.chart) + + splitter = QSplitter(Qt.Orientation.Vertical) + splitter.addWidget(chart_scroll) + splitter.addWidget(self.table) + splitter.setSizes([360, 220]) + + layout = QVBoxLayout(self) + layout.addLayout(controls) + layout.addWidget(self.source_label) + layout.addWidget(splitter) + + self.set_simulation(simulation) + self.load() + + def set_simulation(self, simulation): + if self.simulation is not None: + try: + self.simulation.time_changed.disconnect(self.chart.set_current_time) + except (RuntimeError, TypeError): + pass + self.simulation = simulation + if simulation is not None: + simulation.time_changed.connect(self.chart.set_current_time) + + def set_information(self, mpsoc_config, simulation=None): + self.mpsoc_config = mpsoc_config + self.chart.mpsoc_config = mpsoc_config + self.set_simulation(simulation) + self.load() + + def closeEvent(self, event): + self.set_simulation(None) + super().closeEvent(event) + + def load(self): + """Relê o traffic_router.txt inteiro com um leitor próprio (não mexe na simulação).""" + config = self.mpsoc_config + packets = analysis.read_all_packets(config) + + self.timelines, log_path = analysis.build_timelines(config, packets) + self.trace_end = max((packet.time + packet.bandwidth_cycles for packet in packets), default=1) + if log_path: + self.source_label.setText(f"Request, mapping, release and termination from the mapper log: " + f"{os.path.relpath(log_path, os.path.dirname(config.debug_file_path))}. " + f"Task code arrival and task termination from traffic_router.txt.") + else: + self.source_label.setText("Mapper log not found (log/ folder of the scenario): showing only task " + "code arrival and termination from traffic_router.txt.") + + self.chart.set_timelines(self.timelines, (0, self.trace_end)) + self._apply_zoom() + self._fill_table() + + def _apply_zoom(self, *_): + if self.zoom_combo.currentIndex() == 0 or not self.timelines: + self.chart.set_range(0, self.trace_end) + return + firsts = [t.first_event for t in self.timelines if t.first_event >= 0] + starts = [t.start for t in self.timelines if t.start >= 0] + if not firsts or not starts: + self.chart.set_range(0, self.trace_end) + return + start, end = min(firsts), max(starts) + margin = max(1, (end - start) // 20) + self.chart.set_range(max(0, start - margin), end + margin) + + def _zoom_on_row(self, row, _column): + item = self.table.item(row, 0) + if item is None: + return + timeline = next((t for t in self.timelines if t.app == item.data(ROW_ID_ROLE)), None) + if timeline is None or timeline.first_event < 0 or timeline.start < 0: + return + start, end = timeline.first_event, timeline.start + margin = max(1, (end - start) // 10) + self.chart.set_range(max(0, start - margin), end + margin) + + def _fill_table(self): + config = self.mpsoc_config + + def tick(value): + return (str(value), value) if value >= 0 else ("-", -1) + + rows = [] + for timeline in self.timelines: + admission = timeline.admission_cycles + execution = timeline.execution_cycles + label = analysis.app_label(config, timeline.app) + rows.append([(label, None), + (str(timeline.task_count), timeline.task_count), + tick(timeline.received), tick(timeline.mapped), tick(timeline.start), + tick(timeline.end), tick(timeline.mapping_cycles), tick(admission), + (f"{_ticks_to_us(config, admission):.2f}", admission) if admission >= 0 else ("-", -1), + (f"{_ticks_to_us(config, execution) / 1000:.3f}", execution) + if execution >= 0 else ("-", -1)]) + # O ID da aplicação fica na primeira coluna, para o duplo clique + _fill_table(self.table, rows, [timeline.app for timeline in self.timelines]) diff --git a/testcase_files.py b/testcase_files.py new file mode 100644 index 0000000..f23c7fb --- /dev/null +++ b/testcase_files.py @@ -0,0 +1,100 @@ +""" +Leitura do YAML do testcase do Memphis-V (/.yaml). + +Só a seção de periféricos é usada: + + hw: + Peripherals: + - name: APP_INJ + pe: 2,2 + port: N + +O arquivo é simples o bastante para ser lido linha a linha, sem depender do +PyYAML. +""" + +import os +from dataclasses import dataclass + +SIDES = {"N": "north", "S": "south", "E": "east", "W": "west"} + + +@dataclass(frozen=True) +class Peripheral: + name: str + x: int + y: int + side: str # "N", "S", "E" ou "W" + + +def find_testcase_yaml(mpsoc_config): + """ + /.yaml. O diretório de debug fica em + //debug, então o testcase é dois níveis acima. + """ + scenario = mpsoc_config.get_testcase_path() + if scenario is None: + return None + testcase = os.path.dirname(os.path.normpath(scenario)) + path = os.path.join(testcase, os.path.basename(testcase) + ".yaml") + return path if os.path.isfile(path) else None + + +def _value(line): + return line.split(":", 1)[1].strip().strip("\"'") + + +def parse_peripherals(text): + """Periféricos declarados em hw → Peripherals (linhas inválidas são ignoradas).""" + peripherals = [] + in_section = False + section_indent = 0 + current = {} + + def flush(): + try: + x, y = (int(value) for value in current["pe"].replace("[", "").replace("]", "").split(",")) + side = current["port"].upper()[:1] + if side in SIDES: + peripherals.append(Peripheral(current.get("name", "Peripheral"), x, y, side)) + except (KeyError, ValueError): + pass + + for raw in text.splitlines(): + line = raw.split("#", 1)[0].rstrip() + if not line.strip(): + continue + indent = len(line) - len(line.lstrip()) + stripped = line.strip() + + if stripped.lower().startswith("peripherals:"): + in_section, section_indent, current = True, indent, {} + continue + if not in_section: + continue + if indent <= section_indent: # fim da seção + break + + if stripped.startswith("- "): + if current: + flush() + current = {} + stripped = stripped[2:].strip() + if ":" in stripped: + current[stripped.split(":", 1)[0].strip().lower()] = _value(stripped) + + if in_section and current: + flush() + return peripherals + + +def read_peripherals(mpsoc_config): + """(periféricos, caminho do YAML) — lista vazia e None se o YAML não existir.""" + path = find_testcase_yaml(mpsoc_config) + if path is None: + return [], None + try: + with open(path, "r", encoding="utf-8") as yaml_file: + return parse_peripherals(yaml_file.read()), path + except OSError: + return [], None diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..46626ab --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,100 @@ +""" +Configuração comum dos testes. + +- Qt roda sem janela (QT_QPA_PLATFORM=offscreen). +- QSettings e a pasta de projetos vão para um diretório temporário, para + não mexer no tema, nos projetos e nas pastas salvos pelo usuário. +- O cenário de exemplo é extraído do "DEBUG EXAMPLE.zip" versionado no repositório. +""" + +import os +import tempfile +import zipfile +from pathlib import Path + +_SANDBOX = tempfile.mkdtemp(prefix="memphis-tests-") +os.environ["QT_QPA_PLATFORM"] = "offscreen" +os.environ["XDG_CONFIG_HOME"] = os.path.join(_SANDBOX, "config") +os.environ["XDG_DATA_HOME"] = os.path.join(_SANDBOX, "data") + +import pytest # noqa: E402 + +ROOT = Path(__file__).resolve().parent.parent +EXAMPLE_ZIP = ROOT / "DEBUG EXAMPLE.zip" +SCENARIO_IN_ZIP = "DEBUG EXAMPLE/LOGS/example_testcase/example_scenario/" +TESTCASE_YAML_IN_ZIP = "DEBUG EXAMPLE/LOGS/example_testcase/example_testcase.yaml" + + +@pytest.fixture(scope="session") +def scenario_dir(tmp_path_factory): + """Pasta do cenário de exemplo (contém debug/ e log/).""" + if not EXAMPLE_ZIP.exists(): + pytest.skip("DEBUG EXAMPLE.zip not found") + target = tmp_path_factory.mktemp("example") + with zipfile.ZipFile(EXAMPLE_ZIP) as archive: + members = [name for name in archive.namelist() + if name.startswith(SCENARIO_IN_ZIP) or name == TESTCASE_YAML_IN_ZIP] + archive.extractall(target, members) + return target / SCENARIO_IN_ZIP + + +@pytest.fixture(scope="session") +def debug_dir(scenario_dir): + return str(scenario_dir / "debug") + + +@pytest.fixture(scope="session") +def config(debug_dir): + from util.MPSoCConfig import MPSoCConfig + return MPSoCConfig(debug_dir) + + +@pytest.fixture(scope="session") +def packets(config): + """Todos os pacotes do traffic_router.txt do exemplo.""" + from information.read_traffic_data import ReadTrafficData + from information.router_neighbors import RouterNeighbors + reader = ReadTrafficData(config, RouterNeighbors.from_config(config)) + try: + result = [] + while (packet := reader.get_next_packet()) is not None: + result.append(packet) + return result + finally: + reader.close() + + +@pytest.fixture(scope="session") +def qapp(): + from PySide6.QtWidgets import QApplication + return QApplication.instance() or QApplication([]) + + +@pytest.fixture +def dialogs(monkeypatch): + """ + Substitui as caixas de diálogo modais (que travariam o teste) e registra + as mensagens mostradas. `dialogs.answers` / `dialogs.texts` são as + respostas das próximas perguntas / entradas de texto. + """ + from PySide6.QtWidgets import QInputDialog, QMessageBox + + class Recorder: + def __init__(self): + self.shown = [] + self.answers = [] + self.texts = [] + self.items = [] + + recorder = Recorder() + + def show(kind): + return staticmethod(lambda *args, **kwargs: recorder.shown.append((kind, args[1], args[2]))) + + for kind in ("information", "warning", "critical"): + monkeypatch.setattr(QMessageBox, kind, show(kind)) + monkeypatch.setattr(QMessageBox, "question", staticmethod( + lambda *args, **kwargs: recorder.answers.pop(0) if recorder.answers else QMessageBox.StandardButton.Yes)) + monkeypatch.setattr(QInputDialog, "getText", staticmethod(lambda *args, **kwargs: recorder.texts.pop(0))) + monkeypatch.setattr(QInputDialog, "getItem", staticmethod(lambda *args, **kwargs: recorder.items.pop(0))) + return recorder diff --git a/tests/test_analysis.py b/tests/test_analysis.py new file mode 100644 index 0000000..89d94be --- /dev/null +++ b/tests/test_analysis.py @@ -0,0 +1,287 @@ +"""analysis.py: aplicações, roteamento XY, latência, pacotes parados, deadlock e linha do tempo.""" + +import pytest + +import analysis +from information.packet_information import PacketInformation +from util.MPSoCConfig import MPSoCConfig + +REQUEST = 41 +BROADCAST = 0 + + +@pytest.fixture +def mesh(tmp_path): + """Plataforma sintética 3x3 XY, flits de 32 bits a 100 MHz.""" + (tmp_path / "platform.cfg").write_text( + "router_addressing XY\nchannel_number 2\nmpsoc_x 3\nmpsoc_y 3\nflit_size 32\nclock_period_ns 10\n" + "BEGIN_task_name_relation\ntaskA 256\ntaskB 257\nEND_task_name_relation\n" + "BEGIN_app_name_relation\nmanagement\t0\nsynthetic\t1\nEND_app_name_relation\n") + (tmp_path / "services.cfg").write_text("NEW_APP 0\nMESSAGE_REQUEST 41\nTASK_ALLOCATION 42\n" + "$TASK_ALLOCATION_SERVICE 42\n") + return MPSoCConfig(str(tmp_path)) + + +def at(config, x, y): + return config.xy_to_ham_addr((x << 8) | y) + + +def packet(config, time, router, port, target, service=REQUEST, size=2, bandwidth=6, source=256, target_task=257): + return PacketInformation(at(config, *router), time, service, size, bandwidth, port, + at(config, *target), source, target_task) + + +def path_packets(config, start=100): + """Pacote de 0x0 para 2x1 pelo XY: leste, leste, norte.""" + return [packet(config, start, (0, 0), MPSoCConfig.LOCAL0, (2, 1)), + packet(config, start + 5, (1, 0), MPSoCConfig.WEST0, (2, 1)), + packet(config, start + 10, (2, 0), MPSoCConfig.WEST0, (2, 1)), + packet(config, start + 15, (2, 1), MPSoCConfig.SOUTH0, (2, 1))] + + +# ------------------------------------------------------------------ geometria +def test_next_hop_goes_x_first(mesh): + assert analysis.next_hop(mesh, at(mesh, 0, 0), at(mesh, 2, 1)) == at(mesh, 1, 0) + assert analysis.next_hop(mesh, at(mesh, 2, 0), at(mesh, 2, 1)) == at(mesh, 2, 1) + assert analysis.next_hop(mesh, at(mesh, 2, 1), at(mesh, 2, 1)) is None + + +def test_neighbor_and_border(mesh): + assert analysis.neighbor(mesh, at(mesh, 1, 0), MPSoCConfig.WEST0) == at(mesh, 0, 0) + assert analysis.neighbor(mesh, at(mesh, 0, 0), MPSoCConfig.SOUTH0) is None + assert analysis.is_peripheral_input(mesh, at(mesh, 0, 0), MPSoCConfig.SOUTH0) + assert not analysis.is_peripheral_input(mesh, at(mesh, 1, 1), MPSoCConfig.SOUTH0) + + +@pytest.mark.parametrize("router, port, target, expected", [ + ((1, 0), MPSoCConfig.WEST0, (2, 2), True), # andando para leste, destino à frente em X + ((1, 0), MPSoCConfig.WEST0, (0, 2), False), # andando para leste, destino atrás + ((2, 1), MPSoCConfig.SOUTH0, (2, 2), True), # andando para norte na coluna certa + ((1, 1), MPSoCConfig.SOUTH0, (2, 2), False), # andando em Y antes de terminar o X +]) +def test_xy_consistent(mesh, router, port, target, expected): + assert analysis.xy_consistent(mesh, at(mesh, *router), port, at(mesh, *target)) is expected + + +def test_link_capacity_and_mbps(mesh): + assert analysis.link_capacity_mbps(mesh) == 3200.0 + assert analysis.mbps(mesh, 100, 100) == 3200.0 # 1 flit por ciclo = capacidade total + assert analysis.mbps(mesh, 10, 0) == 0.0 + + +# ---------------------------------------------------------------- aplicações +def test_packet_apps_ignore_kernel_and_unknown(mesh): + known = analysis.known_app_ids(mesh) + assert known == {0, 1} + assert analysis.packet_apps(packet(mesh, 0, (0, 0), 8, (1, 0), source=256, target_task=257), known) == {1} + assert analysis.packet_apps(packet(mesh, 0, (0, 0), 8, (1, 0), source=0xFFFF, target_task=0), known) == {0} + assert analysis.packet_apps(packet(mesh, 0, (0, 0), 8, (1, 0), source=-1, target_task=5000), known) == set() + assert analysis.app_label(mesh, 1) == "1 - synthetic" + + +# --------------------------------------------------------- latência e caminho +def test_task_fields_may_change_between_hops(mesh): + tracker = analysis.TrafficTracker(mesh) + records = path_packets(mesh) + records[3].task_source, records[3].task_target = 0xFFFF, 0 # como no trace do Memphis-V + for record in records: + tracker.consume(record) + [message] = tracker.delivered() + assert message.hops_seen == 3 + assert message.apps == {1} # vale a tarefa da injeção + + +def test_ignores_records_outside_the_mesh(mesh): + tracker = analysis.TrafficTracker(mesh) + record = packet(mesh, 10, (1, 1), MPSoCConfig.LOCAL0, (0, 0), size=0, bandwidth=0) + record.target_router = 512 + tracker.consume(record) + assert tracker.messages == [] + + +def test_tracks_packet_hop_by_hop(mesh): + tracker = analysis.TrafficTracker(mesh) + for record in path_packets(mesh): + tracker.consume(record) + + [message] = tracker.delivered() + assert message.latency == 15 + 6 # chegada do header + ciclos até o último flit sair + assert message.hops == 3 + assert message.hops_seen == 3 + assert message.apps == {1} + assert tracker.pending_count() == 0 + assert analysis.routing_summary(tracker) == ({}, []) + + +def test_path_is_recorded(mesh): + tracker = analysis.TrafficTracker(mesh) + for record in path_packets(mesh): + tracker.consume(record) + [message] = tracker.delivered() + assert [(router, port) for router, port, _time in message.path] == [ + (at(mesh, 0, 0), MPSoCConfig.LOCAL0), (at(mesh, 1, 0), MPSoCConfig.WEST0), + (at(mesh, 2, 0), MPSoCConfig.WEST0), (at(mesh, 2, 1), MPSoCConfig.SOUTH0)] + + +def test_violation_points_to_its_packet(mesh): + tracker = analysis.TrafficTracker(mesh) + # Sai de 1x0 para 2x2 mas sobe antes de terminar o X: 1x0 -> 1x1 (errado) -> 2x1 -> 2x2 + records = [packet(mesh, 10, (1, 0), MPSoCConfig.LOCAL0, (2, 2)), + packet(mesh, 12, (1, 1), MPSoCConfig.SOUTH0, (2, 2)), + packet(mesh, 14, (2, 1), MPSoCConfig.WEST0, (2, 2)), + packet(mesh, 16, (2, 2), MPSoCConfig.SOUTH0, (2, 2))] + for record in records: + tracker.consume(record) + _non_xy, [violation] = analysis.routing_summary(tracker) + assert violation.router == at(mesh, 1, 1) + assert violation.message is tracker.messages[0] + assert tracker.messages[0].hops_seen == 3 + + +def test_messages_in_slice(mesh): + tracker = analysis.TrafficTracker(mesh) + for record in path_packets(mesh, start=100): # entregue em 2x1 no tick 121 + tracker.consume(record) + target, source = at(mesh, 2, 1), at(mesh, 0, 0) + [(message, direction)] = analysis.messages_in_slice(tracker.messages, target, 150, 300) + assert direction == "received" # 29 ciclos antes do trecho + assert analysis.messages_in_slice(tracker.messages, target, 300, 400) == [] + assert analysis.messages_in_slice(tracker.messages, source, 90, 110)[0][1] == "sent" + + +def test_same_router_delivery(mesh): + tracker = analysis.TrafficTracker(mesh) + tracker.consume(packet(mesh, 50, (1, 1), MPSoCConfig.LOCAL0, (1, 1), bandwidth=4)) + [message] = tracker.delivered() + assert (message.latency, message.hops) == (4, 0) + + +def test_missing_intermediate_records_still_match(mesh): + tracker = analysis.TrafficTracker(mesh) + records = path_packets(mesh) + for record in (records[0], records[3]): # só injeção e chegada + tracker.consume(record) + [message] = tracker.delivered() + assert message.latency == 21 + assert message.hops_seen == 0 + + +def test_traffic_by_app(mesh): + tracker = analysis.TrafficTracker(mesh) + for start in (100, 1000): + for record in path_packets(mesh, start): + tracker.consume(record) + stats = analysis.traffic_by_app(tracker.delivered())[1] + assert stats.messages == 2 + assert stats.flits == 6 + assert stats.active_cycles == 1021 - 100 + assert stats.median_latency == 21 + + +# ------------------------------------------------------------------ roteamento +def test_detects_xy_violation(mesh): + tracker = analysis.TrafficTracker(mesh) + for record in path_packets(mesh): + tracker.consume(record) + # Andando para o norte em 1x1, mas o destino está em outra coluna + tracker.consume(packet(mesh, 200, (1, 1), MPSoCConfig.SOUTH0, (2, 2))) + non_xy, anomalies = analysis.routing_summary(tracker) + assert non_xy == {} + assert [(violation.time, violation.router) for violation in anomalies] == [(200, at(mesh, 1, 1))] + + +def test_broadcast_service_is_not_reported_as_violation(mesh): + tracker = analysis.TrafficTracker(mesh) + # Inundação a partir de 0x0: aparece em roteadores fora do caminho XY até 0x1 + for time, router, port in ((10, (1, 0), MPSoCConfig.WEST1), (12, (2, 0), MPSoCConfig.WEST1), + (14, (1, 1), MPSoCConfig.SOUTH1), (16, (2, 1), MPSoCConfig.SOUTH1)): + tracker.consume(packet(mesh, time, router, port, (0, 1), service=BROADCAST, source=-1, target_task=-1)) + non_xy, anomalies = analysis.routing_summary(tracker) + assert BROADCAST in non_xy + assert anomalies == [] + + +# -------------------------------------------------------- parados e deadlock +def test_blocked_packets(mesh): + tracker = analysis.TrafficTracker(mesh) + records = path_packets(mesh) + tracker.consume(records[0]) + tracker.consume(records[1]) # parou em 1x0 + [blocked] = analysis.blocked_messages(tracker, now=20_000, threshold=10_000) + assert blocked.last_router == at(mesh, 1, 0) + assert blocked.hops_seen == 1 + assert analysis.blocked_messages(tracker, now=200, threshold=10_000) == [] + + +def test_circular_wait_is_found(mesh): + corners = [(0, 0), (1, 0), (1, 1), (0, 1)] # cada pacote quer o enlace que o seguinte ocupa + messages = [] + for index, corner in enumerate(corners): + previous = corners[index - 1] + following = corners[(index + 1) % 4] + messages.append(analysis.Message(at(mesh, *previous), at(mesh, *following), REQUEST, 3, 0, + last_router=at(mesh, *corner), last_time=0, + prev_router=at(mesh, *previous), hops_seen=1)) + cycles = analysis.find_wait_cycles(mesh, messages) + assert len(cycles) == 1 + assert len(cycles[0]) == 4 + + +def test_no_cycle_for_independent_packets(mesh): + tracker = analysis.TrafficTracker(mesh) + tracker.consume(path_packets(mesh)[0]) + blocked = analysis.blocked_messages(tracker, now=50_000, threshold=1) + assert analysis.find_wait_cycles(mesh, blocked) == [] + + +# ------------------------------------------------------------ cenário exemplo +def test_example_routing_is_xy(config, packets): + tracker = analysis.TrafficTracker(config) + for record in packets: + tracker.consume(record) + non_xy, anomalies = analysis.routing_summary(tracker) + assert anomalies == [] + names = {config.get_string_service_name(service) for service in non_xy} + assert names == {"NEW_APP", "APP_ALLOCATION_REQUEST", "APP_MAPPING_COMPLETE", "ABORT_TASK"} + + +def test_example_latency(config, packets): + tracker = analysis.TrafficTracker(config) + for record in packets: + tracker.consume(record) + delivered = tracker.delivered() + assert len(tracker.messages) == 1388 # 1398 injeções menos 10 registros sem destino na malha + assert len(delivered) == 1388 + assert tracker.lost == 0 + assert analysis.median([message.latency for message in delivered]) < 50 + # O maior é o código da tarefa mais longa (TASK_ALLOCATION de 2907 flits de payload) + assert max(message.latency for message in delivered) == 13483 + # Todos os pacotes com hops foram seguidos roteador a roteador + multi_hop = [message for message in delivered if message.hops > 0] + assert all(message.hops_seen == message.hops for message in multi_hop) + assert analysis.blocked_messages(tracker, packets[-1].time, 10_000) == [] + + +def test_example_timeline(config, packets): + timelines, log_path = analysis.build_timelines(config, packets) + assert log_path.endswith("log0x0.txt") + by_app = {timeline.app: timeline for timeline in timelines} + assert sorted(by_app) == [0, 1, 2, 3] + + synthetic = by_app[1] + assert synthetic.name == "synthetic_default" + assert synthetic.task_count == 4 + assert (synthetic.received, synthetic.mapped, synthetic.started, synthetic.terminated) == \ + (132778, 164297, 240840, 6037445) + assert synthetic.admission_cycles == 240840 - 132778 + assert sorted(synthetic.task_allocated) == [256, 257, 258, 259] + assert sorted(synthetic.task_terminated) == [256, 257, 258, 259] + + +def test_timeline_without_log(mesh): + records = [PacketInformation(at(mesh, 1, 0), 10, 42, 100, 50, MPSoCConfig.SOUTH0, at(mesh, 1, 0), 256, -1)] + timelines, log_path = analysis.build_timelines(mesh, records) + assert log_path is None + [timeline] = timelines + assert timeline.task_allocated == {256: (60, at(mesh, 1, 0))} + assert timeline.admission_cycles == 0 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..50991f6 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,45 @@ +"""Leitura do platform.cfg / services.cfg e conversões de endereço.""" + +import pytest + +from util.MPSoCConfig import MPSoCConfig + + +def test_platform(config): + assert config.router_addressing == MPSoCConfig.XY + assert (config.mpsoc_x, config.mpsoc_y) == (3, 3) + assert config.flit_size == 32 + assert config.clock_period_in_ns == 10 + assert config.get_pe_number() == 9 + + +def test_task_and_app_names(config): + assert config.task_name_hash[0] == "mapper_task" + assert config.task_name_hash[256] == "taskA" + assert dict(config.app_name_hash) == {0: "management", 1: "synthetic_default", + 2: "prod_cons_custom", 3: "prod_cons_custom"} + + +def test_services(config): + assert config.get_service_value("MESSAGE_REQUEST") == 41 + assert config.get_string_service_name(43) == "MESSAGE_DELIVERY" + assert config.get_service_value("DOES_NOT_EXIST") == -1 + assert 42 in config.task_allocation_services + assert 6 in config.task_terminated_services + + +@pytest.mark.parametrize("address", range(9)) +def test_address_round_trip(config, address): + assert config.xy_to_ham_addr(config.ham_to_xy_addr(address)) == address + label = config.ham_address_to_xy_label(address) + assert config.xy_label_to_ham_address(label) == address + + +def test_hamiltonian_snake(config): + # Linhas ímpares são percorridas da direita para a esquerda + assert config.ham_address_to_xy_label(3) == "2x1" + assert config.ham_address_to_xy_label(5) == "0x1" + + +def test_testcase_path(config, scenario_dir): + assert config.get_testcase_path() == str(scenario_dir).rstrip("/") diff --git a/tests/test_deloream.py b/tests/test_deloream.py new file mode 100644 index 0000000..1369393 --- /dev/null +++ b/tests/test_deloream.py @@ -0,0 +1,50 @@ +from PySide6.QtCore import Qt +from PySide6.QtTest import QTest + +from deloream import DeloreamWindow, read_all_messages, read_task_messages + + +def _item(window, text): + return window.tree.findItems(text, Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchRecursive)[0] + + +def _click(window, item): + rect = window.tree.visualItemRect(item) + QTest.mouseClick(window.tree.viewport(), Qt.MouseButton.LeftButton, pos=rect.center()) + + +def test_read_messages(scenario_dir): + messages = read_all_messages(scenario_dir / "log") + assert sum(len(task_messages) for task_messages in messages.values()) == 409 + assert read_task_messages(scenario_dir / "log", 3, 0) == messages[(3, 0)] + + +def test_single_click_opens_tab(qapp, scenario_dir): + window = DeloreamWindow(str(scenario_dir)) + window.show() + assert window.tabs.count() == 0 + + cons = _item(window, "cons[768]") + _click(window, cons) + assert window.tabs.count() == 1 + tab = window.tabs.currentWidget() + assert tab.kind == "task" and tab.item_id == 768 + assert tab.model.rowCount() == len(window.messages[(3, 0)]) > 0 + + # Clicar de novo volta para a mesma aba + _click(window, cons) + assert window.tabs.count() == 1 + + # Aplicação: mensagens de todas as tarefas, com a coluna da tarefa + _click(window, _item(window, "prod_cons_custom[3]")) + assert window.tabs.count() == 2 + app_tab = window.tabs.currentWidget() + assert app_tab.model.rowCount() == len(window.messages[(3, 0)]) + len(window.messages.get((3, 1), [])) + assert not app_tab.table.isColumnHidden(2) + + app_tab.search.setText("Inicio") + assert 0 < app_tab.proxy.rowCount() < app_tab.model.rowCount() + + window.close_all_tabs() + assert window.tabs.count() == 0 + window.close() diff --git a/tests/test_export.py b/tests/test_export.py new file mode 100644 index 0000000..d6b7bda --- /dev/null +++ b/tests/test_export.py @@ -0,0 +1,31 @@ +"""Exportação CSV/PNG.""" + +import csv + +from PySide6.QtGui import QImage +from PySide6.QtWidgets import QLabel, QTableWidget, QTableWidgetItem + +import export + + +def test_write_csv(qapp, tmp_path): + table = QTableWidget(2, 2) + table.setHorizontalHeaderLabels(["Router", "Flits"]) + for row, values in enumerate((("0x0", "10"), ("1x0", "20, with comma"))): + for column, value in enumerate(values): + table.setItem(row, column, QTableWidgetItem(value)) + + path = tmp_path / "table.csv" + assert export.write_csv(table.model(), str(path)) == 2 + with open(path, newline="", encoding="utf-8") as csv_file: + assert list(csv.reader(csv_file)) == [["Router", "Flits"], ["0x0", "10"], ["1x0", "20, with comma"]] + + +def test_save_png(qapp, tmp_path): + label = QLabel("Memphis") + label.resize(120, 40) + path = tmp_path / "label.png" + export.save_png(label, str(path)) + image = QImage(str(path)) + assert not image.isNull() + assert image.width() >= 120 diff --git a/tests/test_gui.py b/tests/test_gui.py new file mode 100644 index 0000000..36b672e --- /dev/null +++ b/tests/test_gui.py @@ -0,0 +1,219 @@ +"""Testes rápidos das janelas (sem tela, com os diálogos substituídos).""" + +import os + +import pytest +from PySide6.QtWidgets import QDialog + +import projects + + +@pytest.fixture +def window(qapp, dialogs, debug_dir): + import main + main_window = main.MinhaJanela() + assert main_window.load_debug(debug_dir) + yield main_window + main_window.close_router_info_windows() + main_window.stop_simulation() + main_window.close() + main_window.deleteLater() + + +def run_to_end(window): + while window.simulation.next_packet(repaint=False) != -1: + pass + + +def test_load_and_simulate(window): + assert window.windowTitle().endswith("example_scenario") + window.simulation.step() + assert window.simulation_ticks > 0 + run_to_end(window) + assert window.simulation_ticks == 6251176 + + +def test_invalid_directory(qapp, dialogs, tmp_path): + import main + main_window = main.MinhaJanela() + assert not main_window.load_debug(str(tmp_path)) + assert dialogs.shown and dialogs.shown[0][0] == "critical" + + +def test_router_info_window(window): + run_to_end(window) + window.open_router_info(0) + router_window = window.router_info_windows[0] + router_window.refresh() + assert router_window.traffic_table.item(10, 1).text() == "6864 flits" + router_window.log_tab.update_log() + assert "Initializing PE 0" in router_window.log_tab.text.toPlainText() + router_window._open_task_info(0, 0) + assert router_window.task_windows[0].status_label.text() == "Running" + + +def test_application_filter(window): + import analysis + filter_window = window.filter_window + tab = filter_window.app_tab + tab.combo.setCurrentIndex(tab.combo.findData(2)) + tab.add_selected() + tab._apply() + seen = set() + while (packet := window.mpsoc_information.get_next_packet(window.simulation.packet_filter)) is not None: + seen |= analysis.packet_apps(packet, filter_window.known_apps) + assert seen == {2} + + +def test_traffic_statistics_tabs(window): + run_to_end(window) + window.open_traffic_statistics() + statistics = window.traffic_statistics_window + for index in range(statistics.tabs.count()): + statistics.tabs.setCurrentIndex(index) + statistics.refresh() + assert statistics.links_table.rowCount() > 0 + assert statistics.apps_table.rowCount() == 4 + assert statistics.violations_table.rowCount() == 0 + assert "No XY routing violation" in statistics.routing_summary.text() + assert "No packet stopped" in statistics.deadlock_label.text() + assert statistics.blocked_table.rowCount() == 0 + + +def test_statistics_restart_after_going_back(window): + run_to_end(window) + window.open_traffic_statistics() + statistics = window.traffic_statistics_window + statistics.refresh() + total = statistics.tracker.consumed + window.simulation.go_to_time(300000) + statistics.refresh() + assert 0 < statistics.tracker.consumed < total + + +def test_application_timeline(window): + window.open_application_timeline() + timeline = window.timeline_window + assert timeline.table.rowCount() == 4 + timeline.zoom_combo.setCurrentIndex(1) + start, end = timeline.chart.range + assert end < 1_000_000 + + +def test_exports(window, tmp_path, monkeypatch): + import export + from PySide6.QtWidgets import QFileDialog + run_to_end(window) + window.open_traffic_statistics() + statistics = window.traffic_statistics_window + statistics.tabs.setCurrentIndex(1) + statistics.refresh() + + csv_path = tmp_path / "apps.csv" + monkeypatch.setattr(QFileDialog, "getSaveFileName", staticmethod(lambda *a, **k: (str(csv_path), ""))) + statistics._export_current_table() + assert csv_path.read_text().startswith("Application,Messages") + + png_path = tmp_path / "mesh" # extensão é acrescentada + monkeypatch.setattr(QFileDialog, "getSaveFileName", staticmethod(lambda *a, **k: (str(png_path), ""))) + window.save_mesh_png() + assert os.path.getsize(str(png_path) + ".png") > 0 + + +def test_platform_setup(window): + from platform_setup import PlatformSetupDialog + dialog = PlatformSetupDialog(window.mpconfig, window.simulation.checkpoint, window) + dialog.clock_edit.setText("20") + dialog.window_width_edit.setText("250") + dialog.accept() + assert dialog.result() == QDialog.DialogCode.Accepted + assert window.mpconfig.clock_period_in_ns == 20 + assert window.simulation.checkpoint.window_size_ms == 0.25 + + +def test_save_and_open_project(window, dialogs, debug_dir): + dialogs.texts += [("has space", True), ("example", True)] + window.save_project() + assert ("critical", "Error", "Name contains space") in dialogs.shown + assert projects.read_project(projects.project_path("example")) == os.path.abspath(debug_dir) + + window._choose_project = lambda *args: projects.project_path("example") + window.open_project() + assert window.windowTitle().endswith(": example") + + window.delete_project() + assert not os.path.exists(projects.project_path("example")) + + +def test_peripherals_on_mesh(window): + matrix = window.router_matrix_widget + assert matrix.get_router(0, 0).peripherals == {"S": "MA_INJ"} + assert matrix.get_router(2, 2).peripherals == {"N": "APP_INJ"} + assert matrix.get_router(1, 1).peripherals == {} + + +def test_show_path_from_statistics(window): + run_to_end(window) + window.open_traffic_statistics() + statistics = window.traffic_statistics_window + statistics.tabs.setCurrentIndex(2) + statistics.refresh() + # Primeira mensagem com mais de um hop + model = statistics.message_model + row = next(row for row, message in enumerate(model.messages) if message.hops >= 2) + message = model.messages[row] + statistics._show_message_path(statistics.message_filter.mapFromSource(model.index(row, 0))) + + matrix = window.router_matrix_widget + painted = [address for address, router in matrix.routers_by_address.items() if router.need_reset] + assert {router for router, _port, _time in message.path} <= set(painted) + assert matrix.get_router_by_address(message.target).highlighted + assert not window.simulation.running + + window.reset_graphical_path() + assert not any(router.need_reset for router in matrix.routers.values()) + + +def test_scheduling_slice_packets(window): + from router_info_window import SchedulingGraphWindow + graph_window = SchedulingGraphWindow(window.mpconfig, 0) # PE 0x0 (mapper) + graph = graph_window.graph + interruption = next(event for event in graph.events if event.name == "Interruption") + graph.selection_changed.emit(interruption) + assert graph_window.slice_table.rowCount() > 0 + assert graph_window.slice_table.item(0, 1).text() in ("received", "sent") + graph_window._show_slice_message(0, 0) + assert any(router.need_reset for router in window.router_matrix_widget.routers.values()) + graph_window.close() + + +def test_follow_live_trace(qapp, dialogs, debug_dir, tmp_path): + import shutil + + import main + debug = tmp_path / "scenario" / "debug" + shutil.copytree(debug_dir, debug) + traffic = debug / "traffic_router.txt" + lines = traffic.read_text().splitlines(keepends=True) + traffic.write_text("".join(lines[:100])) + + main_window = main.MinhaJanela() + assert main_window.load_debug(str(debug)) + main_window.actionFollow_Live_Trace.setChecked(True) + simulation = main_window.simulation + simulation.play() + while simulation.next_packet(repaint=False) != -1: + pass + assert simulation.running and simulation.waiting # fim do arquivo: continua esperando + assert len(main_window.mpsoc_information.read_traffic.all_packets) == 100 + + with open(traffic, "a") as traffic_file: + traffic_file.write("".join(lines[100:150])) + while simulation.next_packet(repaint=False) != -1: + pass + assert len(main_window.mpsoc_information.read_traffic.all_packets) == 150 + + main_window.actionFollow_Live_Trace.setChecked(False) + assert simulation.next_packet(repaint=False) == -1 + assert not simulation.running # sem follow, o fim do arquivo para a simulação + main_window.close() diff --git a/tests/test_information.py b/tests/test_information.py new file mode 100644 index 0000000..aa23008 --- /dev/null +++ b/tests/test_information.py @@ -0,0 +1,90 @@ +"""Leitura do traffic_router.txt e estado dos roteadores.""" + +from information import MPSoCInformation +from util.MPSoCConfig import MPSoCConfig + + +def test_all_packets_read(packets): + assert len(packets) == 3101 + # O arquivo não está estritamente em ordem de tempo: a linha de um pacote + # longo pode ser gravada depois de registros mais novos + out_of_order = sum(a.time > b.time for a, b in zip(packets, packets[1:])) + assert 0 < out_of_order < len(packets) * 0.05 + + +def run_all(config, packet_filter=None): + information = MPSoCInformation(config) + try: + while information.get_next_packet(packet_filter) is not None: + pass + return information + finally: + information.close() + + +def test_router_volumes(config): + information = run_all(config) + assert information.get_router_information(0).get_router_total_volume_in_flits() == 6864 + assert information.get_total_noc_volume() > 0 + + +def test_terminated_tasks_have_real_ids(config): + # No Memphis-V a tarefa terminada vem no último campo (task_source = -1) + information = run_all(config) + terminated = sorted(task.id for address in range(config.get_pe_number()) + for task in information.get_router_information(address).get_tasks_information() + if task.service == "TERMINATED") + assert terminated == [1, 2, 256, 257, 258, 259, 512, 513, 768, 769] + + +def test_packet_filter_skips_packets(config): + information = run_all(config, lambda packet: packet.router_address == 0) + assert information.get_router_information(4).get_router_total_volume_in_flits() == 0 + assert information.get_router_information(0).get_router_total_volume_in_flits() == 6864 + + +def test_go_back_in_time(config): + information = MPSoCInformation(config) + try: + for _ in range(100): + information.get_next_packet() + time = information.read_traffic.all_packets[49].time + counter = information.get_packet_counter_by_time(time) + assert counter >= 50 + assert information.get_packet_counter_by_time(10 ** 12) == -1 + finally: + information.close() + + +def test_port_names(): + assert MPSoCConfig.get_port_string(MPSoCConfig.LOCAL1) == "LOCAL 1" + assert MPSoCConfig.get_port_string(99) is None + + +def test_follow_reads_lines_as_they_are_written(tmp_path, debug_dir): + import shutil + + from information.read_traffic_data import ReadTrafficData + from information.router_neighbors import RouterNeighbors + from util.MPSoCConfig import MPSoCConfig + + debug = tmp_path / "debug" + shutil.copytree(debug_dir, debug) + lines = (debug / "traffic_router.txt").read_text().splitlines(keepends=True) + traffic = debug / "traffic_router.txt" + traffic.write_text("".join(lines[:2]) + lines[2][:5]) # terceira linha pela metade + + config = MPSoCConfig(str(debug)) + reader = ReadTrafficData(config, RouterNeighbors.from_config(config)) + reader.follow = True + try: + assert reader.get_next_packet() is not None + assert reader.get_next_packet() is not None + assert reader.get_next_packet() is None # linha incompleta: espera + with open(traffic, "a") as traffic_file: + traffic_file.write(lines[2][5:] + lines[3]) + assert reader.get_next_packet().time == int(lines[2].split("\t")[0]) + assert reader.get_next_packet().time == int(lines[3].split("\t")[0]) + assert reader.get_next_packet() is None + finally: + reader.close() diff --git a/tests/test_testcase_files.py b/tests/test_testcase_files.py new file mode 100644 index 0000000..3d8a44c --- /dev/null +++ b/tests/test_testcase_files.py @@ -0,0 +1,44 @@ +"""Periféricos do YAML do testcase.""" + +from testcase_files import Peripheral, find_testcase_yaml, parse_peripherals, read_peripherals + +YAML = """ +hw: # Hardware properties + mpsoc_dimension: [3,3] + Peripherals: # Attached peripherals + - name: MA_INJ # comentário + pe: 0,0 + port: S + - name: APP_INJ + pe: [2,2] + port: North + - name: BROKEN # sem porta: ignorado + pe: 1,1 + parameters: + - TRAFFIC_DEBUG: yes +""" + + +def test_parse_peripherals(): + assert parse_peripherals(YAML) == [Peripheral("MA_INJ", 0, 0, "S"), Peripheral("APP_INJ", 2, 2, "N")] + + +def test_no_peripherals_section(): + assert parse_peripherals("hw:\n mpsoc_dimension: [3,3]\n") == [] + + +def test_example_testcase(config): + peripherals, path = read_peripherals(config) + assert path.endswith("example_testcase.yaml") + assert peripherals == [Peripheral("MA_INJ", 0, 0, "S"), Peripheral("APP_INJ", 2, 2, "N")] + + +def test_missing_yaml(tmp_path): + from util.MPSoCConfig import MPSoCConfig + debug = tmp_path / "scenario" / "debug" + debug.mkdir(parents=True) + (debug / "platform.cfg").write_text("mpsoc_x 2\nmpsoc_y 2\n") + (debug / "services.cfg").write_text("") + config = MPSoCConfig(str(debug)) + assert find_testcase_yaml(config) is None + assert read_peripherals(config) == ([], None) diff --git a/theme.py b/theme.py index 8a71817..5e25da4 100644 --- a/theme.py +++ b/theme.py @@ -24,10 +24,18 @@ class MatrixColors: canvas: str # fundo atrás dos roteadores text: str # textos fora do corpo do roteador (ex.: % da porta local) cluster_border: str # borda entre clusters - - -LIGHT_MATRIX = MatrixColors(canvas="#ffffff", text="#000000", cluster_border="#808080") -DARK_MATRIX = MatrixColors(canvas="#262626", text="#e6e6e6", cluster_border="#6e6e6e") + arrow: str # enlace sem pacote passando + arrow_active: str # enlace por onde o pacote atual passou + peripheral_fill: str + peripheral_text: str # também usado na borda da caixa do periférico + + +LIGHT_MATRIX = MatrixColors(canvas="#ffffff", text="#000000", cluster_border="#808080", + arrow="#8c8c8c", arrow_active="#e00000", + peripheral_fill="#fff1e0", peripheral_text="#b34700") +DARK_MATRIX = MatrixColors(canvas="#262626", text="#e6e6e6", cluster_border="#6e6e6e", + arrow="#7a7a7a", arrow_active="#ff4040", + peripheral_fill="#3d2a1a", peripheral_text="#ffb070") def matrix_colors(dark): diff --git a/util/MPSoCConfig.py b/util/MPSoCConfig.py index 0a0607b..7bc3021 100644 --- a/util/MPSoCConfig.py +++ b/util/MPSoCConfig.py @@ -74,6 +74,7 @@ def __init__(self, debug_file_path): self.services_hash = SortedDict() self.task_name_hash = SortedDict() + self.app_name_hash = SortedDict() self.service_reference = [] self.task_allocation_services = [] self.task_terminated_services = [] @@ -116,6 +117,8 @@ def __init__(self, debug_file_path): case "BEGIN_task_name_relation": # Passamos o OBJETO do arquivo, não a string do caminho self.initialize_task_naming(platform_file) + case "BEGIN_app_name_relation": + self.initialize_app_naming(platform_file) case _: pass except FileNotFoundError: @@ -150,6 +153,19 @@ def initialize_task_naming(self, platform_file): continue + def initialize_app_naming(self, platform_file): + """Lê a relação nomeID das aplicações (até END_app_name_relation).""" + self.app_name_hash = SortedDict() + for line in platform_file: + fields = line.split() + if fields == ["END_app_name_relation"]: + break + if len(fields) >= 2: + try: + self.app_name_hash[int(fields[1])] = fields[0] + except ValueError: + continue + def initialize_services(self, services_file_path): """Lê os serviços (nome e código decimal) do arquivo services.cfg.""" self.services_hash = SortedDict() diff --git a/uv.lock b/uv.lock index 092019e..3236fb4 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,42 @@ version = 1 revision = 3 requires-python = ">=3.14" +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "project-memphis-py" version = "0.1.0" @@ -11,12 +47,29 @@ dependencies = [ { name = "sortedcontainers" }, ] +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + [package.metadata] requires-dist = [ { name = "pyside6", specifier = ">=6.7" }, { name = "sortedcontainers", specifier = ">=2.4.0" }, ] +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=9.1.1" }] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + [[package]] name = "pyside6" version = "6.11.2" @@ -65,6 +118,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cd/87/4dd2a6468cea600da1fcf9c7f02bc248eeaa98cdc75658e48e669d95326a/pyside6_essentials-6.11.2-cp310-abi3-win_arm64.whl", hash = "sha256:fadd75c5c20800d64dd0a586ea8cb337c5e630aa2246df0e5105738afa46e02c", size = 58125520, upload-time = "2026-08-18T06:36:59.073Z" }, ] +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + [[package]] name = "shiboken6" version = "6.11.2"