gini-toolkit 6.0.1.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gini/__init__.py +12 -0
- gini/__main__.py +107 -0
- gini/_version.py +24 -0
- gini/agent/__init__.py +17 -0
- gini/agent/agent_gamemaster.py +140 -0
- gini/agent/api.py +291 -0
- gini/agent/ask.py +123 -0
- gini/agent/authoring.py +72 -0
- gini/agent/blackboard.py +114 -0
- gini/agent/contracts.py +142 -0
- gini/agent/domains.py +91 -0
- gini/agent/embed.py +123 -0
- gini/agent/gamemaster.py +256 -0
- gini/agent/kb.py +148 -0
- gini/agent/lesson_resolver.py +261 -0
- gini/agent/llm/__init__.py +5 -0
- gini/agent/llm/backend.py +43 -0
- gini/agent/llm/fake.py +25 -0
- gini/agent/llm/ollama.py +206 -0
- gini/agent/loop.py +258 -0
- gini/agent/mcp_server.py +86 -0
- gini/agent/meaning.py +225 -0
- gini/agent/mission.py +210 -0
- gini/agent/mission_controller.py +208 -0
- gini/agent/narration.py +116 -0
- gini/agent/notifier.py +86 -0
- gini/agent/personas.py +79 -0
- gini/agent/reasoning.py +172 -0
- gini/agent/recall.py +248 -0
- gini/agent/session.py +79 -0
- gini/agent/teaching_center.py +482 -0
- gini/agent/tools/__init__.py +3 -0
- gini/agent/tools/registry.py +193 -0
- gini/agent/twin/__init__.py +28 -0
- gini/agent/twin/authoring.py +71 -0
- gini/agent/twin/contracts.py +54 -0
- gini/agent/twin/dialectic.py +189 -0
- gini/agent/twin/harness.py +93 -0
- gini/agent/twin/justify.py +156 -0
- gini/agent/twin/learner.py +64 -0
- gini/agent/twin/mission.py +60 -0
- gini/agent/twin/os_coach.py +79 -0
- gini/agent/twin/salience.py +30 -0
- gini/agent/understand.py +250 -0
- gini/agent/verifiers.py +106 -0
- gini/agent/wizard.py +178 -0
- gini/agent/xv6_pack.py +74 -0
- gini/app/__init__.py +3 -0
- gini/app/context.py +368 -0
- gini/app/paths.py +121 -0
- gini/data/README.md +21 -0
- gini/domain/__init__.py +9 -0
- gini/domain/assembly.py +209 -0
- gini/domain/authoring.py +353 -0
- gini/domain/blueprints.py +5 -0
- gini/domain/capabilities.py +177 -0
- gini/domain/catalog.py +85 -0
- gini/domain/certify.py +201 -0
- gini/domain/compose.py +413 -0
- gini/domain/composition.py +88 -0
- gini/domain/concepts.py +383 -0
- gini/domain/connection_rules.py +269 -0
- gini/domain/constraints.py +153 -0
- gini/domain/content.py +59 -0
- gini/domain/cpu_journey.py +89 -0
- gini/domain/devices.py +747 -0
- gini/domain/diagnose.py +201 -0
- gini/domain/element_guide.py +327 -0
- gini/domain/explain.py +90 -0
- gini/domain/fingerprint.py +201 -0
- gini/domain/firewall.py +34 -0
- gini/domain/flowlog.py +61 -0
- gini/domain/flowtable.py +179 -0
- gini/domain/fragment_yaml.py +230 -0
- gini/domain/fragments.py +169 -0
- gini/domain/games/__init__.py +2 -0
- gini/domain/games/paging_games.py +119 -0
- gini/domain/games/policy_game.py +86 -0
- gini/domain/games/process_game.py +48 -0
- gini/domain/games/thrash_game.py +75 -0
- gini/domain/games/translate_game.py +60 -0
- gini/domain/games/trap_game.py +86 -0
- gini/domain/grader.py +155 -0
- gini/domain/grouping.py +67 -0
- gini/domain/legality.py +103 -0
- gini/domain/lesson.py +241 -0
- gini/domain/lexicon.py +150 -0
- gini/domain/machine_state.py +410 -0
- gini/domain/missions/networking/basic-lan.yaml +32 -0
- gini/domain/missions/networking/cache-in-front.yaml +23 -0
- gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
- gini/domain/missions/networking/drive-load.yaml +20 -0
- gini/domain/missions/networking/fix-the-address.yaml +75 -0
- gini/domain/missions/networking/fix-the-lan.yaml +43 -0
- gini/domain/missions/networking/inspect-flows.yaml +16 -0
- gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
- gini/domain/missions/networking/least-privilege.yaml +21 -0
- gini/domain/missions/networking/load-balanced-web.yaml +29 -0
- gini/domain/missions/networking/observe-it.yaml +24 -0
- gini/domain/missions/networking/put-in-vpc.yaml +30 -0
- gini/domain/missions/networking/reachability-boundary.yaml +56 -0
- gini/domain/missions/networking/sdn-reactive.yaml +35 -0
- gini/domain/missions/networking/send-request.yaml +19 -0
- gini/domain/missions/networking/serverless-api.yaml +25 -0
- gini/domain/missions/networking/service-chain.yaml +33 -0
- gini/domain/missions/os/lottery-fix.yaml +19 -0
- gini/domain/missions/os/priority-fix.yaml +24 -0
- gini/domain/missions.py +111 -0
- gini/domain/modulechain.py +36 -0
- gini/domain/objectives.py +488 -0
- gini/domain/os_zoo.py +79 -0
- gini/domain/paging_sim.py +141 -0
- gini/domain/pricing.py +199 -0
- gini/domain/probes.py +226 -0
- gini/domain/profile.py +142 -0
- gini/domain/recipes.py +738 -0
- gini/domain/riders.py +309 -0
- gini/domain/router_modules.py +224 -0
- gini/domain/routetable.py +67 -0
- gini/domain/scoring.py +76 -0
- gini/domain/staging.py +122 -0
- gini/domain/syscall_builder.py +144 -0
- gini/domain/topic_cloud.py +62 -0
- gini/domain/topology.py +213 -0
- gini/domain/vocabulary.py +51 -0
- gini/domain/xv6.py +808 -0
- gini/domain/xv6_fs.py +250 -0
- gini/domain/xv6_runner.py +113 -0
- gini/domain/xv6_vm.py +385 -0
- gini/gloader.py +17 -0
- gini/runtime/__init__.py +18 -0
- gini/runtime/cloudfabric_agent.py +370 -0
- gini/runtime/console.py +68 -0
- gini/runtime/control.py +70 -0
- gini/runtime/frame.py +138 -0
- gini/runtime/gbridge.py +638 -0
- gini/runtime/grouter.py +223 -0
- gini/runtime/hostsim.py +90 -0
- gini/runtime/shuttle.py +348 -0
- gini/runtime/switch.py +109 -0
- gini/runtime/transport.py +77 -0
- gini/runtime/xv6_bridge.py +312 -0
- gini/server/__init__.py +22 -0
- gini/server/__main__.py +74 -0
- gini/server/app.py +140 -0
- gini/server/auth.py +82 -0
- gini/server/policy.py +57 -0
- gini/server/session.py +23 -0
- gini/services/__init__.py +15 -0
- gini/services/boardflash.py +248 -0
- gini/services/boardsetup.py +374 -0
- gini/services/cloud_catalog.py +143 -0
- gini/services/compiler.py +1858 -0
- gini/services/discovery.py +324 -0
- gini/services/gloader.py +183 -0
- gini/services/orchestrator.py +1460 -0
- gini/services/persistence.py +28 -0
- gini/services/probe_runner.py +149 -0
- gini/services/project.py +217 -0
- gini/services/remote.py +93 -0
- gini/services/rider_runner.py +96 -0
- gini/services/rider_session.py +171 -0
- gini/services/shadow_store.py +52 -0
- gini/services/terminal.py +45 -0
- gini/setup/__init__.py +17 -0
- gini/setup/cli.py +109 -0
- gini/setup/images.py +33 -0
- gini/setup/marker.py +43 -0
- gini/setup/runtime.py +69 -0
- gini/ui/__init__.py +3 -0
- gini/ui/assets/app_icon.icns +0 -0
- gini/ui/assets/app_icon.ico +0 -0
- gini/ui/assets/app_icon.png +0 -0
- gini/ui/assets/app_icon_1024.png +0 -0
- gini/ui/assets/cue/_w.txt +1 -0
- gini/ui/assets/cue/ai.png +0 -0
- gini/ui/assets/cue/canvas.png +0 -0
- gini/ui/assets/cue/cloud.png +0 -0
- gini/ui/assets/cue/cost.png +0 -0
- gini/ui/assets/cue/dark/ai.png +0 -0
- gini/ui/assets/cue/dark/canvas.png +0 -0
- gini/ui/assets/cue/dark/cloud.png +0 -0
- gini/ui/assets/cue/dark/cost.png +0 -0
- gini/ui/assets/cue/dark/metrics.png +0 -0
- gini/ui/assets/cue/dark/router.png +0 -0
- gini/ui/assets/cue/dark/run.png +0 -0
- gini/ui/assets/cue/dark/serverless.png +0 -0
- gini/ui/assets/cue/dark/settings.png +0 -0
- gini/ui/assets/cue/dark/welcome.png +0 -0
- gini/ui/assets/cue/dark/wizard.png +0 -0
- gini/ui/assets/cue/ginibrand/ai.png +0 -0
- gini/ui/assets/cue/ginibrand/canvas.png +0 -0
- gini/ui/assets/cue/ginibrand/cloud.png +0 -0
- gini/ui/assets/cue/ginibrand/cost.png +0 -0
- gini/ui/assets/cue/ginibrand/metrics.png +0 -0
- gini/ui/assets/cue/ginibrand/router.png +0 -0
- gini/ui/assets/cue/ginibrand/run.png +0 -0
- gini/ui/assets/cue/ginibrand/serverless.png +0 -0
- gini/ui/assets/cue/ginibrand/settings.png +0 -0
- gini/ui/assets/cue/ginibrand/welcome.png +0 -0
- gini/ui/assets/cue/ginibrand/wizard.png +0 -0
- gini/ui/assets/cue/highcontrast/ai.png +0 -0
- gini/ui/assets/cue/highcontrast/canvas.png +0 -0
- gini/ui/assets/cue/highcontrast/cloud.png +0 -0
- gini/ui/assets/cue/highcontrast/cost.png +0 -0
- gini/ui/assets/cue/highcontrast/metrics.png +0 -0
- gini/ui/assets/cue/highcontrast/router.png +0 -0
- gini/ui/assets/cue/highcontrast/run.png +0 -0
- gini/ui/assets/cue/highcontrast/serverless.png +0 -0
- gini/ui/assets/cue/highcontrast/settings.png +0 -0
- gini/ui/assets/cue/highcontrast/welcome.png +0 -0
- gini/ui/assets/cue/highcontrast/wizard.png +0 -0
- gini/ui/assets/cue/light/ai.png +0 -0
- gini/ui/assets/cue/light/canvas.png +0 -0
- gini/ui/assets/cue/light/cloud.png +0 -0
- gini/ui/assets/cue/light/cost.png +0 -0
- gini/ui/assets/cue/light/metrics.png +0 -0
- gini/ui/assets/cue/light/router.png +0 -0
- gini/ui/assets/cue/light/run.png +0 -0
- gini/ui/assets/cue/light/serverless.png +0 -0
- gini/ui/assets/cue/light/settings.png +0 -0
- gini/ui/assets/cue/light/welcome.png +0 -0
- gini/ui/assets/cue/light/wizard.png +0 -0
- gini/ui/assets/cue/metrics.png +0 -0
- gini/ui/assets/cue/router.png +0 -0
- gini/ui/assets/cue/run.png +0 -0
- gini/ui/assets/cue/serverless.png +0 -0
- gini/ui/assets/cue/settings.png +0 -0
- gini/ui/assets/cue/welcome.png +0 -0
- gini/ui/assets/cue/wizard.png +0 -0
- gini/ui/assistant.py +2111 -0
- gini/ui/author_dialog.py +184 -0
- gini/ui/board_dialog.py +247 -0
- gini/ui/branding.py +21 -0
- gini/ui/canvas.py +2007 -0
- gini/ui/chat_panel.py +7 -0
- gini/ui/cpu_journey.py +212 -0
- gini/ui/cpu_lab.py +306 -0
- gini/ui/cue_cards.py +214 -0
- gini/ui/dashboard.py +222 -0
- gini/ui/diagnose_game.py +336 -0
- gini/ui/fingerprint_lab.py +219 -0
- gini/ui/flash_dialog.py +244 -0
- gini/ui/flow_layout.py +63 -0
- gini/ui/fragment_manager.py +1415 -0
- gini/ui/game_catalog.py +184 -0
- gini/ui/game_renderers.py +340 -0
- gini/ui/games_lab.py +90 -0
- gini/ui/inspector.py +1055 -0
- gini/ui/live_metrics.py +130 -0
- gini/ui/machine_lab.py +1412 -0
- gini/ui/main_window.py +3153 -0
- gini/ui/memory_lab.py +371 -0
- gini/ui/mission_panel.py +302 -0
- gini/ui/mode_indicator.py +227 -0
- gini/ui/palette.py +112 -0
- gini/ui/peripherals.py +218 -0
- gini/ui/process_tree.py +130 -0
- gini/ui/reset_dialog.py +179 -0
- gini/ui/router_lab.py +776 -0
- gini/ui/run_button.py +183 -0
- gini/ui/settings_dialog.py +234 -0
- gini/ui/signin_dialog.py +111 -0
- gini/ui/storage_lab.py +219 -0
- gini/ui/syscall_builder.py +235 -0
- gini/ui/syscall_lab.py +152 -0
- gini/ui/theme/__init__.py +5 -0
- gini/ui/theme/icons.py +145 -0
- gini/ui/theme/manager.py +291 -0
- gini/ui/theme/tokens.py +194 -0
- gini/ui/trap_lab.py +270 -0
- gini/ui/worker_host.py +102 -0
- gini/ui/zoo_lab.py +112 -0
- gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
- gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
- gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
- gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
- gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
gini/ui/machine_lab.py
ADDED
|
@@ -0,0 +1,1412 @@
|
|
|
1
|
+
"""Machine Lab — the visual OS workbench for an xv6 Machine.
|
|
2
|
+
|
|
3
|
+
Open it by double-clicking an xv6 Machine. The scheduler face is the flagship: four live
|
|
4
|
+
panels — process table, CPU registers, memory (address space / SATP), and the kernel stack —
|
|
5
|
+
that update on each context switch, plus a Gantt strip of which process held the CPU and a
|
|
6
|
+
switch counter. Controls let you slow the time-slice to watch switches happen, Step one
|
|
7
|
+
context switch at a time, or free-run.
|
|
8
|
+
|
|
9
|
+
The lab reads state through an injected `provider` (the same Snapshot shape whether it comes
|
|
10
|
+
from QEMU's GDB stub on the Mac or the offline DemoScheduler here), so all of the UI is
|
|
11
|
+
testable offscreen against a fake feed — the same split as the Router Lab.
|
|
12
|
+
|
|
13
|
+
Mirror of the Router Lab: there you step a *packet* through a pipeline; here you step the
|
|
14
|
+
*CPU* through context switches.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from PySide6.QtCore import Qt, QTimer, Signal
|
|
19
|
+
from PySide6.QtGui import QColor, QPainter
|
|
20
|
+
from PySide6.QtWidgets import (
|
|
21
|
+
QCheckBox, QComboBox, QDialog, QFrame, QGridLayout, QHBoxLayout, QHeaderView, QLabel, QLineEdit,
|
|
22
|
+
QPlainTextEdit, QPushButton, QScrollArea, QSizePolicy, QSlider, QSpinBox, QStackedWidget,
|
|
23
|
+
QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from ..domain.machine_state import MachineState
|
|
27
|
+
from ..domain.xv6 import DemoScheduler, policy_name, ready_queue, short_pid
|
|
28
|
+
from .theme import ThemeManager, icons
|
|
29
|
+
from .theme.manager import scale_css as _scss
|
|
30
|
+
|
|
31
|
+
# scheduler policies the selector offers (must match domain POLICY_NAMES / kernel gini_pick)
|
|
32
|
+
_POLICIES = ["round-robin", "priority", "lottery"]
|
|
33
|
+
|
|
34
|
+
# long-running programs the launcher offers (must match the agent's PROGRAMS list)
|
|
35
|
+
_LAUNCHABLE = ["spin", "busy", "alloc", "writer", "grind", "forktest"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _pid_color(pid) -> str:
|
|
39
|
+
"""A UNIQUE, well-separated colour per pid — golden-angle hue rotation, so no two nearby
|
|
40
|
+
pids share a colour (the old 6-colour cycle collided constantly)."""
|
|
41
|
+
if pid is None:
|
|
42
|
+
return "#666666"
|
|
43
|
+
import colorsys
|
|
44
|
+
h = ((pid * 137.508) % 360) / 360.0 # golden angle -> maximally spread hues
|
|
45
|
+
r, g, b = colorsys.hsv_to_rgb(h, 0.58, 0.88)
|
|
46
|
+
return "#%02x%02x%02x" % (int(r * 255), int(g * 255), int(b * 255))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class LayerCard(QFrame):
|
|
50
|
+
"""A clickable card in the layered overview: title + one-line description + a live mini-stat.
|
|
51
|
+
Cards are grouped into OS layers (user / syscall interface / kernel / hardware) so the front
|
|
52
|
+
door reads like the textbook stack — the student drills into a component instead of being
|
|
53
|
+
dropped straight into the dense scheduler view."""
|
|
54
|
+
|
|
55
|
+
clicked = Signal()
|
|
56
|
+
|
|
57
|
+
def __init__(self, theme, title, desc, accent_key) -> None:
|
|
58
|
+
super().__init__()
|
|
59
|
+
self.theme = theme
|
|
60
|
+
self._accent = accent_key
|
|
61
|
+
t = theme.theme
|
|
62
|
+
acc = t.accent_for(accent_key)
|
|
63
|
+
self.setCursor(Qt.PointingHandCursor)
|
|
64
|
+
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
|
65
|
+
self.setMinimumHeight(92)
|
|
66
|
+
self.setStyleSheet(
|
|
67
|
+
f"LayerCard{{background:{t.panel};border:1px solid {t.line};border-radius:12px;}}"
|
|
68
|
+
f"LayerCard:hover{{border-color:{acc};background:{t.panel2};}}")
|
|
69
|
+
v = QVBoxLayout(self); v.setContentsMargins(12, 10, 12, 10); v.setSpacing(3)
|
|
70
|
+
row = QHBoxLayout(); row.setSpacing(6)
|
|
71
|
+
dot = QLabel("●"); dot.setStyleSheet(f"color:{acc};font-size:11px;border:none;")
|
|
72
|
+
ttl = QLabel(title)
|
|
73
|
+
ttl.setStyleSheet(_scss(f"color:{t.text};font-size:14px;font-weight:600;border:none;"))
|
|
74
|
+
row.addWidget(dot); row.addWidget(ttl); row.addStretch(1)
|
|
75
|
+
v.addLayout(row)
|
|
76
|
+
d = QLabel(desc); d.setWordWrap(True)
|
|
77
|
+
d.setStyleSheet(_scss(f"color:{t.muted};font-size:11px;border:none;"))
|
|
78
|
+
v.addWidget(d)
|
|
79
|
+
v.addStretch(1)
|
|
80
|
+
self.stat = QLabel("")
|
|
81
|
+
self.stat.setStyleSheet(
|
|
82
|
+
_scss(f"color:{acc};font-family:monospace;font-size:12px;font-weight:600;border:none;"))
|
|
83
|
+
v.addWidget(self.stat)
|
|
84
|
+
|
|
85
|
+
def set_stat(self, text) -> None:
|
|
86
|
+
self.stat.setText(text or "")
|
|
87
|
+
|
|
88
|
+
def mouseReleaseEvent(self, e) -> None: # noqa: N802
|
|
89
|
+
if e.button() == Qt.LeftButton and self.rect().contains(e.position().toPoint()):
|
|
90
|
+
self.clicked.emit()
|
|
91
|
+
super().mouseReleaseEvent(e)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class SchedulingPanel(QWidget):
|
|
95
|
+
"""The scheduler's decision, made legible: the READY QUEUE (who's waiting, ordered the way the
|
|
96
|
+
policy favours) plus the CPU SHARE each process actually got. The Gantt shows who-ran-over-time;
|
|
97
|
+
this shows who's-next-and-why + how fairly the CPU was split — the evidence a lottery/priority
|
|
98
|
+
assignment is graded on. Read-only: students change priority/tickets from their own program."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, theme: ThemeManager) -> None:
|
|
101
|
+
super().__init__()
|
|
102
|
+
self.theme = theme
|
|
103
|
+
t = theme.theme
|
|
104
|
+
v = QVBoxLayout(self); v.setContentsMargins(0, 0, 0, 0); v.setSpacing(4)
|
|
105
|
+
self._tbl = QTableWidget(0, 6)
|
|
106
|
+
self._tbl.setHorizontalHeaderLabels(["pid", "name", "pri", "tickets", "wait", "CPU share"])
|
|
107
|
+
self._tbl.verticalHeader().setVisible(False)
|
|
108
|
+
self._tbl.setEditTriggers(QTableWidget.NoEditTriggers)
|
|
109
|
+
self._tbl.setSelectionMode(QTableWidget.NoSelection)
|
|
110
|
+
self._tbl.horizontalHeader().setStretchLastSection(True)
|
|
111
|
+
self._tbl.setStyleSheet(
|
|
112
|
+
f"QTableWidget{{background:{t.panel};color:{t.text};border:none;"
|
|
113
|
+
f"gridline-color:{t.line};font-family:monospace;font-size:11px;}}"
|
|
114
|
+
f"QHeaderView::section{{background:{t.panel2};color:{t.muted};border:none;"
|
|
115
|
+
"padding:3px;font-family:sans-serif;}")
|
|
116
|
+
v.addWidget(self._tbl)
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _bar(frac: float, width: int = 8) -> str:
|
|
120
|
+
fill = max(0, min(width, round(frac * width)))
|
|
121
|
+
return "█" * fill + "░" * (width - fill) + f" {frac * 100:.0f}%"
|
|
122
|
+
|
|
123
|
+
def update_view(self, procs, shares) -> None:
|
|
124
|
+
t = self.theme.theme
|
|
125
|
+
running = [p for p in procs if p.state == "running"]
|
|
126
|
+
rows = running + ready_queue(procs) # running first, then the ready queue
|
|
127
|
+
self._tbl.setRowCount(len(rows))
|
|
128
|
+
for i, p in enumerate(rows):
|
|
129
|
+
share = shares.get(p.pid, 0.0)
|
|
130
|
+
vals = [str(p.pid), p.name,
|
|
131
|
+
"—" if p.priority is None else str(p.priority),
|
|
132
|
+
"—" if p.tickets is None else str(p.tickets),
|
|
133
|
+
"—" if p.wait_ticks is None else str(p.wait_ticks),
|
|
134
|
+
self._bar(share)]
|
|
135
|
+
for c, val in enumerate(vals):
|
|
136
|
+
item = QTableWidgetItem(val)
|
|
137
|
+
if p.state == "running":
|
|
138
|
+
item.setForeground(QColor(t.accent_for("green")))
|
|
139
|
+
self._tbl.setItem(i, c, item)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class GanttStrip(QWidget):
|
|
143
|
+
"""A horizontal strip of recent scheduling slots — one cell per snapshot, coloured by the
|
|
144
|
+
running pid, so context switches read as colour changes across time."""
|
|
145
|
+
|
|
146
|
+
def __init__(self, theme, label="") -> None:
|
|
147
|
+
super().__init__()
|
|
148
|
+
self.theme = theme
|
|
149
|
+
self.label = label # e.g. "CPU 0" (shown at the left on SMP)
|
|
150
|
+
self._slots: list = []
|
|
151
|
+
self.setMinimumHeight(46)
|
|
152
|
+
|
|
153
|
+
def set_slots(self, slots) -> None:
|
|
154
|
+
self._slots = list(slots)
|
|
155
|
+
self.update()
|
|
156
|
+
|
|
157
|
+
def paintEvent(self, _e) -> None:
|
|
158
|
+
p = QPainter(self)
|
|
159
|
+
t = self.theme.theme
|
|
160
|
+
h = self.height()
|
|
161
|
+
p.fillRect(self.rect(), QColor(t.panel2))
|
|
162
|
+
left = 44 if self.label else 4 # left gutter for the "CPU i" label
|
|
163
|
+
right = 96 # right gutter for the running-pid text
|
|
164
|
+
if self.label:
|
|
165
|
+
p.setPen(QColor(t.muted))
|
|
166
|
+
p.drawText(0, 0, 40, h, Qt.AlignVCenter | Qt.AlignRight, self.label)
|
|
167
|
+
area = self.width() - left - right
|
|
168
|
+
if not self._slots or area <= 4:
|
|
169
|
+
p.setPen(QColor(t.muted))
|
|
170
|
+
p.drawText(self.rect().adjusted(left, 0, 0, 0), Qt.AlignCenter, "— press Run —")
|
|
171
|
+
return
|
|
172
|
+
n = len(self._slots)
|
|
173
|
+
w = max(2.0, area / max(n, 1))
|
|
174
|
+
for i, s in enumerate(self._slots): # the colour band fills the full strip height
|
|
175
|
+
x = left + int(i * w)
|
|
176
|
+
p.fillRect(x, 3, int(w) + 1, h - 6, QColor(_pid_color(s.pid)))
|
|
177
|
+
if w >= 16 and s.pid is not None and (i == 0 or self._slots[i - 1].pid != s.pid):
|
|
178
|
+
p.setPen(QColor("#111111"))
|
|
179
|
+
# last two digits, so a narrow segment never centre-clips a full pid to a
|
|
180
|
+
# misleading middle (3615 & 3613 both showed '61'); full pid is in the right gutter.
|
|
181
|
+
p.drawText(x, 3, int(w), h - 6, Qt.AlignCenter, short_pid(s.pid))
|
|
182
|
+
last = self._slots[-1] # current pid in the reserved right gutter
|
|
183
|
+
p.setPen(QColor(t.text))
|
|
184
|
+
who = "idle" if last.pid is None else f"pid {last.pid}"
|
|
185
|
+
if h >= 40 and last.name:
|
|
186
|
+
who += f" {last.name}"
|
|
187
|
+
p.drawText(self.width() - right + 6, 0, right - 8, h, Qt.AlignVCenter | Qt.AlignLeft, who)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class MachineLab(QDialog):
|
|
191
|
+
"""Scheduler face of an xv6 Machine (the only face today; linux/kata later)."""
|
|
192
|
+
|
|
193
|
+
snap_ready = Signal(object) # a Snapshot pushed from a worker thread (live mode)
|
|
194
|
+
load_result = Signal(bool, str, str) # (ok, log, action) from a Load/Revert worker thread
|
|
195
|
+
shadows_ready = Signal(object) # {name: ShadowStatus} from a worker thread
|
|
196
|
+
|
|
197
|
+
def __init__(self, parent, theme: ThemeManager, device, state: MachineState | None = None,
|
|
198
|
+
live=False, on_console=None, on_log=None) -> None:
|
|
199
|
+
super().__init__(parent)
|
|
200
|
+
self.theme = theme
|
|
201
|
+
self.device = device
|
|
202
|
+
self.on_console = on_console
|
|
203
|
+
self.on_log = on_log # (level, message) -> mirror to the GINI Console
|
|
204
|
+
self._shadows: dict = {} # last shadow manifest (name -> ShadowStatus)
|
|
205
|
+
self._shadow_present_seen = False # for the "shadow detected" one-shot console line
|
|
206
|
+
self._reenable_shadow = None # after a Load, re-run the shadow once the kernel is back
|
|
207
|
+
# The shared MachineState is the bridge (owns provider + timeline + watcher); the Lab
|
|
208
|
+
# only renders from it. With no state we spin up a demo-backed one so the lab is explorable.
|
|
209
|
+
self.state = state or MachineState(
|
|
210
|
+
DemoScheduler(timeslice=int((device.properties or {}).get("Timeslice", "1") or "1")),
|
|
211
|
+
device_id=getattr(device, "id", ""), mode="demo")
|
|
212
|
+
# `live` (behaviour: async reads, launcher, poll-vs-step) follows the DATA MODE, which is
|
|
213
|
+
# a user choice on the state — Real means live kernel, Demo means the stand-in feed.
|
|
214
|
+
self.live = (self.state.mode == "real")
|
|
215
|
+
self._running = False
|
|
216
|
+
|
|
217
|
+
t = theme.theme
|
|
218
|
+
self.setWindowTitle(f"Machine Lab — {device.name}")
|
|
219
|
+
self.resize(940, 700)
|
|
220
|
+
self.setStyleSheet(f"QDialog{{background:{t.bg};}}")
|
|
221
|
+
|
|
222
|
+
root = QVBoxLayout(self)
|
|
223
|
+
self._build_topbar(root)
|
|
224
|
+
|
|
225
|
+
# Two rooms behind one door: the layered OVERVIEW (page 0, shown first) and the dense
|
|
226
|
+
# SCHEDULER view (page 1). The scheduler page is built eagerly so its widgets exist for
|
|
227
|
+
# rendering + tests even while the overview is on screen.
|
|
228
|
+
self._stack = QStackedWidget()
|
|
229
|
+
root.addWidget(self._stack, 1)
|
|
230
|
+
|
|
231
|
+
self._sched_page = QWidget()
|
|
232
|
+
spl = QVBoxLayout(self._sched_page); spl.setContentsMargins(0, 0, 0, 0)
|
|
233
|
+
shdr = QHBoxLayout() # title header, consistent with the other Labs' windows
|
|
234
|
+
sic = QLabel(); sic.setPixmap(icons.render_pixmap("host", t.accent_for("red"), 22))
|
|
235
|
+
stitle = QLabel(f" Process Scheduler Lab — {device.name}")
|
|
236
|
+
stitle.setStyleSheet(_scss(f"color:{t.text};font-size:16px;font-weight:600;"))
|
|
237
|
+
shdr.addWidget(sic); shdr.addWidget(stitle); shdr.addStretch(1)
|
|
238
|
+
spl.addLayout(shdr)
|
|
239
|
+
self._build_banner(spl) # shown in Real mode when there's no live data (never fake)
|
|
240
|
+
self._build_controls(spl)
|
|
241
|
+
if self.live:
|
|
242
|
+
self._build_launcher(spl) # launch/kill programs to give the scheduler real work
|
|
243
|
+
self._build_sched_controls(spl) # per-proc priority/tickets (control-plane) setters
|
|
244
|
+
self._build_shadow_bar(spl) # the shadow: toggle + Load/Revert + inline result
|
|
245
|
+
self._build_panels(spl)
|
|
246
|
+
|
|
247
|
+
self._overview = self._build_overview()
|
|
248
|
+
self._stack.addWidget(self._overview) # index 0 — the hub is the permanent Machine Lab
|
|
249
|
+
# the scheduler page opens in its OWN window (like every other card) so the hub stays up and
|
|
250
|
+
# subsystems can be open concurrently; built eagerly (widgets exist for rendering + tests),
|
|
251
|
+
# reparented into its window on first open.
|
|
252
|
+
self._sched_win = None
|
|
253
|
+
|
|
254
|
+
self._busy = False
|
|
255
|
+
self._closed = False # set on close; guards worker-thread callbacks
|
|
256
|
+
self.snap_ready.connect(self._on_snap)
|
|
257
|
+
self.load_result.connect(self._on_load_result)
|
|
258
|
+
self.shadows_ready.connect(self._on_shadows)
|
|
259
|
+
self._poll = QTimer(self)
|
|
260
|
+
self._poll.timeout.connect(self._on_poll)
|
|
261
|
+
self._ov_poll = QTimer(self) # slow refresh so overview mini-stats stay live
|
|
262
|
+
self._ov_poll.timeout.connect(self._on_ov_poll)
|
|
263
|
+
self._shadow_poll = QTimer(self) # slow poll for shadow status (catches file edits)
|
|
264
|
+
self._shadow_poll.timeout.connect(self._on_shadow_poll)
|
|
265
|
+
|
|
266
|
+
self._show_overview()
|
|
267
|
+
self._render() # initial paint (state's first snapshot is taken when it's created)
|
|
268
|
+
if self.live:
|
|
269
|
+
self._fetch(step=False) # kick an async read so the stats/table populate on open
|
|
270
|
+
|
|
271
|
+
# -- top bar (persistent across pages) -------------------------------- #
|
|
272
|
+
def _build_topbar(self, root) -> None:
|
|
273
|
+
t = self.theme.theme
|
|
274
|
+
head = QHBoxLayout()
|
|
275
|
+
self._back_btn = QPushButton(" ← Overview")
|
|
276
|
+
self._back_btn.setStyleSheet(self._btn_css())
|
|
277
|
+
self._back_btn.clicked.connect(self._show_overview)
|
|
278
|
+
self._back_btn.setVisible(False)
|
|
279
|
+
head.addWidget(self._back_btn)
|
|
280
|
+
ic = QLabel(); ic.setPixmap(icons.render_pixmap("host", t.accent_for("red"), 24))
|
|
281
|
+
self._title_lbl = QLabel(f" Machine Lab — {self.device.name}")
|
|
282
|
+
self._title_lbl.setStyleSheet(_scss(f"color:{t.text};font-size:16px;font-weight:600;"))
|
|
283
|
+
head.addWidget(ic); head.addWidget(self._title_lbl); head.addStretch(1)
|
|
284
|
+
# Real/Demo is a user choice (never auto-switched). Real = live kernel; Demo = the stand-in
|
|
285
|
+
# feed for exploring the interface with nothing running.
|
|
286
|
+
from PySide6.QtWidgets import QButtonGroup
|
|
287
|
+
dlbl = QLabel("data"); dlbl.setStyleSheet(_scss(f"color:{t.faint};font-size:11px;"))
|
|
288
|
+
head.addWidget(dlbl)
|
|
289
|
+
self._mode_group = QButtonGroup(self); self._mode_group.setExclusive(True)
|
|
290
|
+
self._mode_btns = {}
|
|
291
|
+
for key, label in (("real", "Real"), ("demo", "Demo")):
|
|
292
|
+
b = QPushButton(label); b.setCheckable(True)
|
|
293
|
+
b.setChecked(self.state.mode == key)
|
|
294
|
+
b.setStyleSheet(self._mode_btn_css())
|
|
295
|
+
b.clicked.connect(lambda _c=False, k=key: self._set_data_mode(k))
|
|
296
|
+
self._mode_group.addButton(b); self._mode_btns[key] = b
|
|
297
|
+
head.addWidget(b)
|
|
298
|
+
root.addLayout(head)
|
|
299
|
+
|
|
300
|
+
def _build_banner(self, root) -> None:
|
|
301
|
+
"""A 'no live data' banner for Real mode with nothing running — an explicit error that
|
|
302
|
+
offers Demo, instead of silently painting fake data."""
|
|
303
|
+
t = self.theme.theme
|
|
304
|
+
self._banner = QFrame()
|
|
305
|
+
self._banner.setStyleSheet(
|
|
306
|
+
f"QFrame{{background:{t.panel2};border:1px solid {t.accent_for('amber')};"
|
|
307
|
+
"border-radius:10px;}")
|
|
308
|
+
lay = QHBoxLayout(self._banner); lay.setContentsMargins(12, 8, 12, 8)
|
|
309
|
+
self._banner_lbl = QLabel()
|
|
310
|
+
self._banner_lbl.setWordWrap(True)
|
|
311
|
+
self._banner_lbl.setStyleSheet(_scss(f"color:{t.text};font-size:12px;border:none;"))
|
|
312
|
+
lay.addWidget(self._banner_lbl, 1)
|
|
313
|
+
use_demo = QPushButton(" Use Demo")
|
|
314
|
+
use_demo.setStyleSheet(self._btn_css())
|
|
315
|
+
use_demo.clicked.connect(lambda: self._set_data_mode("demo"))
|
|
316
|
+
lay.addWidget(use_demo)
|
|
317
|
+
self._banner.setVisible(False)
|
|
318
|
+
root.addWidget(self._banner)
|
|
319
|
+
|
|
320
|
+
def _mode_btn_css(self) -> str:
|
|
321
|
+
t = self.theme.theme
|
|
322
|
+
return (f"QPushButton{{color:{t.muted};background:{t.panel2};border:1px solid {t.line};"
|
|
323
|
+
f"border-radius:8px;padding:4px 12px;font-size:12px;}}"
|
|
324
|
+
f"QPushButton:checked{{color:{t.accent_for('green')};border-color:{t.accent};"
|
|
325
|
+
f"background:{t.panel};font-weight:600;}}"
|
|
326
|
+
f"QPushButton:hover{{border-color:{t.accent};}}")
|
|
327
|
+
|
|
328
|
+
def _set_data_mode(self, mode: str) -> None:
|
|
329
|
+
"""User flipped Real/Demo. Swap the state's data plane, realign behaviour, and repaint.
|
|
330
|
+
NEVER called automatically — the state also never auto-falls-back, so Real stays Real."""
|
|
331
|
+
if self._running:
|
|
332
|
+
self._toggle_run() # stop the poll loop before swapping the source
|
|
333
|
+
self.state.set_mode(mode)
|
|
334
|
+
self.live = (mode == "real")
|
|
335
|
+
for k, b in self._mode_btns.items():
|
|
336
|
+
b.setChecked(k == mode)
|
|
337
|
+
self._render()
|
|
338
|
+
if self.live and self.state.has_real():
|
|
339
|
+
self._fetch(step=False) # kick a live read on entering Real
|
|
340
|
+
|
|
341
|
+
# -- overview: the layered OS stack of drill-down cards --------------- #
|
|
342
|
+
def _build_overview(self) -> QWidget:
|
|
343
|
+
t = self.theme.theme
|
|
344
|
+
page = QWidget()
|
|
345
|
+
outer = QVBoxLayout(page); outer.setContentsMargins(0, 0, 0, 0)
|
|
346
|
+
intro = QLabel("The xv6 machine, layer by layer. Pick a component to open it — you drill "
|
|
347
|
+
"into detail as you need it.")
|
|
348
|
+
intro.setWordWrap(True)
|
|
349
|
+
intro.setStyleSheet(_scss(f"color:{t.muted};font-size:12px;"))
|
|
350
|
+
outer.addWidget(intro)
|
|
351
|
+
|
|
352
|
+
scroll = QScrollArea(); scroll.setWidgetResizable(True)
|
|
353
|
+
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
|
354
|
+
scroll.setStyleSheet("QScrollArea{border:none;background:transparent;}")
|
|
355
|
+
body = QWidget(); col = QVBoxLayout(body); col.setContentsMargins(2, 2, 2, 2)
|
|
356
|
+
col.setSpacing(6)
|
|
357
|
+
self._ov_cards: dict = {}
|
|
358
|
+
|
|
359
|
+
# top of the stack: what the student's programs are, running in user mode
|
|
360
|
+
col.addWidget(self._layer_band("USER SPACE", [
|
|
361
|
+
("programs", "Programs & Shell", "The processes you launch — running in user mode.",
|
|
362
|
+
"green", self._open_console),
|
|
363
|
+
("games", "Games", "Diagnose-from-the-signature challenges.",
|
|
364
|
+
"purple", self._open_games)]))
|
|
365
|
+
col.addWidget(self._boundary("ecall ▾ trap into the kernel · sret ▴ back to user"))
|
|
366
|
+
# the system-call interface: the door between user and kernel
|
|
367
|
+
col.addWidget(self._layer_band("SYSTEM-CALL INTERFACE", [
|
|
368
|
+
("syscalls", "System Calls", "Live histogram (last 60s) + strace-style trace.",
|
|
369
|
+
"blue", self._open_syscall_lab),
|
|
370
|
+
("builder", "Syscall Builder", "Add your own syscall — real kernel edits generated.",
|
|
371
|
+
"red", self._open_syscall_builder),
|
|
372
|
+
("fingerprints", "Process Fingerprints",
|
|
373
|
+
"Each process's behavioral signature + a classify game.",
|
|
374
|
+
"purple", self._open_fingerprints)]))
|
|
375
|
+
col.addWidget(self._boundary("supervisor mode · the kernel"))
|
|
376
|
+
# the kernel's core subsystems
|
|
377
|
+
col.addWidget(self._layer_band("KERNEL", [
|
|
378
|
+
("scheduler", "Process Scheduler", "Watch the CPU move between processes.",
|
|
379
|
+
"red", self._show_scheduler),
|
|
380
|
+
("memory", "Virtual Memory", "Page tables, the allocator, and page faults.",
|
|
381
|
+
"purple", self._open_memory_lab),
|
|
382
|
+
("storage", "File System", "Inodes, buffer cache, and the write-ahead log.",
|
|
383
|
+
"cyan", self._open_storage_lab),
|
|
384
|
+
("journey", "Traps & Interrupts", "Live trap mix (syscall/fault/timer) + step one.",
|
|
385
|
+
"amber", self._open_trap_lab)]))
|
|
386
|
+
col.addWidget(self._boundary("registers · trapframe · timer interrupts"))
|
|
387
|
+
# the hardware the kernel drives
|
|
388
|
+
col.addWidget(self._layer_band("HARDWARE", [
|
|
389
|
+
("cpu", "CPU & Registers", "Per-core registers, satp, and the kernel stack.",
|
|
390
|
+
"red", self._open_cpu)]))
|
|
391
|
+
col.addStretch(1)
|
|
392
|
+
scroll.setWidget(body)
|
|
393
|
+
outer.addWidget(scroll, 1)
|
|
394
|
+
return page
|
|
395
|
+
|
|
396
|
+
def _layer_band(self, name, cards) -> QFrame:
|
|
397
|
+
t = self.theme.theme
|
|
398
|
+
band = QFrame()
|
|
399
|
+
band.setStyleSheet(
|
|
400
|
+
f"QFrame{{background:{t.panel2};border:1px solid {t.line};border-radius:12px;}}")
|
|
401
|
+
v = QVBoxLayout(band); v.setContentsMargins(12, 8, 12, 12); v.setSpacing(8)
|
|
402
|
+
lbl = QLabel(name)
|
|
403
|
+
lbl.setStyleSheet(_scss(f"color:{t.faint};font-size:10px;font-weight:700;"
|
|
404
|
+
"letter-spacing:2px;border:none;"))
|
|
405
|
+
v.addWidget(lbl)
|
|
406
|
+
row = QHBoxLayout(); row.setSpacing(8)
|
|
407
|
+
for key, title, desc, accent, handler in cards:
|
|
408
|
+
card = LayerCard(self.theme, title, desc, accent)
|
|
409
|
+
card.clicked.connect(handler)
|
|
410
|
+
self._ov_cards[key] = card
|
|
411
|
+
row.addWidget(card)
|
|
412
|
+
v.addLayout(row)
|
|
413
|
+
return band
|
|
414
|
+
|
|
415
|
+
def _boundary(self, text) -> QLabel:
|
|
416
|
+
t = self.theme.theme
|
|
417
|
+
lbl = QLabel(text); lbl.setAlignment(Qt.AlignCenter)
|
|
418
|
+
lbl.setStyleSheet(_scss(f"color:{t.faint};font-size:10px;letter-spacing:1px;"))
|
|
419
|
+
return lbl
|
|
420
|
+
|
|
421
|
+
# -- page navigation -------------------------------------------------- #
|
|
422
|
+
def _show_overview(self) -> None:
|
|
423
|
+
self._stack.setCurrentWidget(self._overview)
|
|
424
|
+
self._back_btn.setVisible(False) # vestigial now the hub is permanent
|
|
425
|
+
self._title_lbl.setText(f" Machine Lab — {self.device.name}")
|
|
426
|
+
if self.live:
|
|
427
|
+
self._ov_poll.start(2000) # keep the mini-stats fresh while the hub is up
|
|
428
|
+
self._update_overview()
|
|
429
|
+
|
|
430
|
+
def _ensure_sched_window(self) -> None:
|
|
431
|
+
"""Lazily wrap the eagerly-built scheduler page in its own window (reparents it once)."""
|
|
432
|
+
if self._sched_win is not None:
|
|
433
|
+
return
|
|
434
|
+
from PySide6.QtWidgets import QDialog
|
|
435
|
+
t = self.theme.theme
|
|
436
|
+
w = QDialog(self)
|
|
437
|
+
w.setWindowTitle(f"Process Scheduler Lab — {self.device.name}")
|
|
438
|
+
w.resize(940, 700)
|
|
439
|
+
w.setStyleSheet(f"QDialog{{background:{t.bg};}}")
|
|
440
|
+
lay = QVBoxLayout(w); lay.setContentsMargins(10, 10, 10, 10)
|
|
441
|
+
lay.addWidget(self._sched_page)
|
|
442
|
+
w.finished.connect(lambda _r=0: self._shadow_poll.stop())
|
|
443
|
+
self._sched_win = w
|
|
444
|
+
|
|
445
|
+
def _show_scheduler(self) -> None:
|
|
446
|
+
# opens the scheduler in its OWN window; the hub (and its mini-stat poll) stays live, so the
|
|
447
|
+
# scheduler can be open alongside Memory/CPU/etc.
|
|
448
|
+
self._ensure_sched_window()
|
|
449
|
+
self._sched_win.show()
|
|
450
|
+
self._sched_win.raise_()
|
|
451
|
+
if self.live:
|
|
452
|
+
self._shadow_poll.start(3000) # keep the shadow status fresh; catch external edits
|
|
453
|
+
self._refresh_shadows()
|
|
454
|
+
|
|
455
|
+
def _on_ov_poll(self) -> None:
|
|
456
|
+
"""Slow overview refresh: read current state (live) so the cards' mini-stats update."""
|
|
457
|
+
if self.live and not self._closed:
|
|
458
|
+
self._fetch(step=False)
|
|
459
|
+
|
|
460
|
+
def _require_data(self) -> bool:
|
|
461
|
+
"""Guard the data-driven faces: in Real mode with nothing running there's no provider to
|
|
462
|
+
read, so surface the banner (on the scheduler page) instead of opening an empty dialog."""
|
|
463
|
+
if self.state.mode == "real" and not self.state.has_real():
|
|
464
|
+
self._show_scheduler() # the banner lives here and explains + offers Demo
|
|
465
|
+
self._update_banner()
|
|
466
|
+
return False
|
|
467
|
+
return True
|
|
468
|
+
|
|
469
|
+
def _open_syscall_lab(self) -> None:
|
|
470
|
+
if not self._require_data():
|
|
471
|
+
return
|
|
472
|
+
from .syscall_lab import SyscallLab
|
|
473
|
+
# live /sc over the serial when running; DemoScheduler.sc() offline
|
|
474
|
+
src = getattr(self.state.provider, "sc", None)
|
|
475
|
+
self._sclab = SyscallLab(self, self.theme, device=self.device,
|
|
476
|
+
sc_source=src if callable(src) else None)
|
|
477
|
+
self._sclab.show(); self._sclab.raise_()
|
|
478
|
+
|
|
479
|
+
def _open_trap_lab(self) -> None:
|
|
480
|
+
# The Traps room: the live trap-cause histogram + feed (observational front), with a
|
|
481
|
+
# "Step a trap" button into the frame-by-frame journey. Not gated on live data — the
|
|
482
|
+
# histogram shows an empty state, but the (authored) journey is always reachable.
|
|
483
|
+
from .trap_lab import TrapLab
|
|
484
|
+
src = getattr(self.state.provider, "traps", None)
|
|
485
|
+
catch = getattr(self.state.provider, "catch_trap", None) # live gdb freeze (Phase 2/4)
|
|
486
|
+
alarms = getattr(self.state.provider, "alarms", None) # sigalarm-lab strip (Phase 3)
|
|
487
|
+
self._traplab = TrapLab(self, self.theme, device=self.device,
|
|
488
|
+
traps_source=src if callable(src) else None,
|
|
489
|
+
catch_source=catch if callable(catch) else None,
|
|
490
|
+
alarm_source=alarms if callable(alarms) else None,
|
|
491
|
+
on_step=self._open_journey,
|
|
492
|
+
on_play=lambda: self._play_game("trap-cause"))
|
|
493
|
+
self._traplab.show(); self._traplab.raise_()
|
|
494
|
+
|
|
495
|
+
def _open_journey(self, frame=None) -> None:
|
|
496
|
+
# Seed the journey with a frozen trap (real scause/sepc/stval + saved regs) when we have
|
|
497
|
+
# one; otherwise fall back to the running proc's registers at the dispatch stage.
|
|
498
|
+
from .cpu_journey import CpuJourney
|
|
499
|
+
cpu = self.state.latest.cpu if (self.state.latest and self.state.latest.cpu) else None
|
|
500
|
+
self._journey = CpuJourney(self, self.theme, device=self.device, cpu=cpu, frame=frame)
|
|
501
|
+
self._journey.show(); self._journey.raise_()
|
|
502
|
+
|
|
503
|
+
def _open_games(self) -> None:
|
|
504
|
+
from .games_lab import GamesLab
|
|
505
|
+
self._games = GamesLab(self, self.theme, self.device, self.state, live=self.live)
|
|
506
|
+
self._games.show()
|
|
507
|
+
self._games.raise_()
|
|
508
|
+
|
|
509
|
+
def _play_game(self, game_id: str) -> None:
|
|
510
|
+
from .games_lab import open_game
|
|
511
|
+
self._game_win = open_game(self, self.theme, self.device, self.state, game_id, self.live)
|
|
512
|
+
|
|
513
|
+
def _open_fingerprints(self) -> None:
|
|
514
|
+
# Cross-cutting behavioral view (syscalls + traps + scheduling). Not gated on live data:
|
|
515
|
+
# in Demo it uses canned fingerprints so the panel + classify game work offline.
|
|
516
|
+
from .fingerprint_lab import FingerprintLab
|
|
517
|
+
self._fplab = FingerprintLab(self, self.theme, self.device, self.state, live=self.live)
|
|
518
|
+
self._fplab.show()
|
|
519
|
+
self._fplab.raise_()
|
|
520
|
+
|
|
521
|
+
def _open_cpu(self) -> None:
|
|
522
|
+
# The HARDWARE face: per-core register file, decoded satp, kernel stack. Distinct from the
|
|
523
|
+
# Process Scheduler (which process runs) — this is the registers the CPU runs *with*.
|
|
524
|
+
if not self._require_data():
|
|
525
|
+
return
|
|
526
|
+
from .cpu_lab import CpuLab
|
|
527
|
+
self._cpulab = CpuLab(self, self.theme, self.device, self.state, live=self.live)
|
|
528
|
+
self._cpulab.show()
|
|
529
|
+
self._cpulab.raise_()
|
|
530
|
+
|
|
531
|
+
def _open_memory_lab(self) -> None:
|
|
532
|
+
if not self._require_data():
|
|
533
|
+
return
|
|
534
|
+
from .memory_lab import MemoryLab
|
|
535
|
+
# render from the shared MachineState's VM reader (demo stand-in or the Mac GDB bridge),
|
|
536
|
+
# so the Memory face and the Ask GINI card see one source.
|
|
537
|
+
self._memory = MemoryLab(self, self.theme, device=self.device, provider=self.state.vm,
|
|
538
|
+
on_play=self._play_game,
|
|
539
|
+
play_games=[("diagnose thrashing", "thrash-diagnose"),
|
|
540
|
+
("translate an address", "addr-translate")])
|
|
541
|
+
self._memory.show()
|
|
542
|
+
self._memory.raise_()
|
|
543
|
+
|
|
544
|
+
def _open_storage_lab(self) -> None:
|
|
545
|
+
if not self._require_data():
|
|
546
|
+
return
|
|
547
|
+
from .storage_lab import StorageLab
|
|
548
|
+
self._storage = StorageLab(self, self.theme, device=self.device, provider=self.state.fs)
|
|
549
|
+
self._storage.show()
|
|
550
|
+
self._storage.raise_()
|
|
551
|
+
|
|
552
|
+
def _open_syscall_builder(self) -> None:
|
|
553
|
+
from .syscall_builder import SyscallBuilder
|
|
554
|
+
# If the live provider knows how to write+recompile (Mac-side), let Apply drive it;
|
|
555
|
+
# offline the builder still generates and previews the exact code.
|
|
556
|
+
apply_fn = getattr(self.state.provider, "apply_syscall", None)
|
|
557
|
+
self._syscalls = SyscallBuilder(
|
|
558
|
+
self, self.theme, device=self.device,
|
|
559
|
+
on_apply=apply_fn if callable(apply_fn) else None)
|
|
560
|
+
self._syscalls.show()
|
|
561
|
+
self._syscalls.raise_()
|
|
562
|
+
|
|
563
|
+
def _btn_css(self) -> str:
|
|
564
|
+
t = self.theme.theme
|
|
565
|
+
return (f"QPushButton{{color:{t.text};background:{t.panel2};border:1px solid {t.line};"
|
|
566
|
+
f"border-radius:8px;padding:6px 12px;}}"
|
|
567
|
+
f"QPushButton:hover{{border-color:{t.accent};}}"
|
|
568
|
+
f"QPushButton:disabled{{color:{t.faint};}}")
|
|
569
|
+
|
|
570
|
+
# -- controls (time-slice + step/run) --------------------------------- #
|
|
571
|
+
def _build_controls(self, root) -> None:
|
|
572
|
+
t = self.theme.theme
|
|
573
|
+
hint = QLabel("Watch the kernel move the CPU between processes. Slow the time-slice or "
|
|
574
|
+
"Step one context switch at a time to see it happen.")
|
|
575
|
+
hint.setWordWrap(True)
|
|
576
|
+
hint.setStyleSheet(_scss(f"color:{t.muted};font-size:11px;"))
|
|
577
|
+
root.addWidget(hint)
|
|
578
|
+
bar = QFrame(); bar.setStyleSheet(
|
|
579
|
+
_scss(f"QFrame{{background:{t.panel2};border:1px solid {t.line};border-radius:10px;}}"))
|
|
580
|
+
lay = QHBoxLayout(bar); lay.setContentsMargins(12, 8, 12, 8)
|
|
581
|
+
|
|
582
|
+
lbl = QLabel("Time-slice"); lbl.setStyleSheet(_scss(f"color:{t.muted};font-size:12px;"))
|
|
583
|
+
lay.addWidget(lbl)
|
|
584
|
+
self._slice = QSlider(Qt.Horizontal)
|
|
585
|
+
# quantum in timer ticks (~0.5s each) -> 1..10 ticks = 0.5..5s slices.
|
|
586
|
+
self._slice.setMinimum(1); self._slice.setMaximum(10)
|
|
587
|
+
self._slice.setValue(min(self.state.timeslice, 10))
|
|
588
|
+
self._slice.setFixedWidth(180)
|
|
589
|
+
# label tracks the drag smoothly; the actual (blocking, kernel-halting) quantum write
|
|
590
|
+
# happens ONCE on release, off the GUI thread — dragging must not flood the kernel.
|
|
591
|
+
self._slice.valueChanged.connect(self._update_slice_lbl)
|
|
592
|
+
self._slice.sliderReleased.connect(self._apply_slice)
|
|
593
|
+
lay.addWidget(self._slice)
|
|
594
|
+
self._slice_lbl = QLabel(); self._slice_lbl.setStyleSheet(
|
|
595
|
+
_scss(f"color:{t.text};font-size:12px;min-width:64px;"))
|
|
596
|
+
lay.addWidget(self._slice_lbl)
|
|
597
|
+
|
|
598
|
+
# scheduler policy selector — switches RR/priority/lottery LIVE (over the serial). The
|
|
599
|
+
# kernel confirmation next to it shows the policy the kernel actually reports.
|
|
600
|
+
plbl = QLabel(" Policy"); plbl.setStyleSheet(_scss(f"color:{t.muted};font-size:12px;"))
|
|
601
|
+
lay.addWidget(plbl)
|
|
602
|
+
self._policy_combo = QComboBox()
|
|
603
|
+
self._policy_combo.addItems(_POLICIES)
|
|
604
|
+
cur = getattr(self.state, "policy", "round-robin")
|
|
605
|
+
if cur in _POLICIES:
|
|
606
|
+
self._policy_combo.setCurrentText(cur)
|
|
607
|
+
self._policy_combo.setStyleSheet(
|
|
608
|
+
f"QComboBox{{color:{t.text};background:{t.panel};border:1px solid {t.line};"
|
|
609
|
+
"border-radius:6px;padding:3px 8px;}")
|
|
610
|
+
self._policy_combo.currentTextChanged.connect(self._apply_policy) # connect AFTER setting
|
|
611
|
+
lay.addWidget(self._policy_combo)
|
|
612
|
+
self._policy_kernel_lbl = QLabel()
|
|
613
|
+
self._policy_kernel_lbl.setStyleSheet(_scss(f"color:{t.faint};font-size:11px;"))
|
|
614
|
+
lay.addWidget(self._policy_kernel_lbl)
|
|
615
|
+
# in-lab game: read a Gantt, name the policy
|
|
616
|
+
self._policy_play = QPushButton(" Play")
|
|
617
|
+
self._policy_play.setToolTip("Guess-the-scheduler game: name the policy from a timeline")
|
|
618
|
+
self._policy_play.setIcon(icons.icon("robot", t.accent_for("purple"), 14))
|
|
619
|
+
self._policy_play.setStyleSheet(self._btn_css())
|
|
620
|
+
self._policy_play.clicked.connect(lambda: self._play_game("guess-policy"))
|
|
621
|
+
lay.addWidget(self._policy_play)
|
|
622
|
+
lay.addStretch(1)
|
|
623
|
+
|
|
624
|
+
self._step_btn = QPushButton(" Step switch")
|
|
625
|
+
self._step_btn.setIcon(icons.icon("send", t.accent_for("red"), 15))
|
|
626
|
+
self._step_btn.clicked.connect(self._on_step)
|
|
627
|
+
self._step_btn.setStyleSheet(self._btn_css())
|
|
628
|
+
lay.addWidget(self._step_btn)
|
|
629
|
+
|
|
630
|
+
self._run_btn = QPushButton(" Run")
|
|
631
|
+
self._run_btn.setIcon(icons.icon("play", t.accent_for("green"), 15))
|
|
632
|
+
self._run_btn.clicked.connect(self._toggle_run)
|
|
633
|
+
self._run_btn.setStyleSheet(self._btn_css())
|
|
634
|
+
lay.addWidget(self._run_btn)
|
|
635
|
+
|
|
636
|
+
self._switch_lbl = QLabel("0 switches")
|
|
637
|
+
self._switch_lbl.setStyleSheet(_scss(f"color:{t.muted};font-size:12px;min-width:88px;"))
|
|
638
|
+
lay.addWidget(self._switch_lbl)
|
|
639
|
+
root.addWidget(bar)
|
|
640
|
+
self._update_slice_lbl()
|
|
641
|
+
|
|
642
|
+
# one Gantt strip per CPU (SMP). Built on demand from the per-CPU timelines.
|
|
643
|
+
self._gantt_box = QVBoxLayout(); self._gantt_box.setSpacing(3)
|
|
644
|
+
holder = QWidget(); holder.setLayout(self._gantt_box)
|
|
645
|
+
root.addWidget(holder)
|
|
646
|
+
self._gantts: dict = {} # cpu_index -> GanttStrip
|
|
647
|
+
self._gantt = GanttStrip(self.theme) # the default single strip (cpu 0)
|
|
648
|
+
self._gantts[0] = self._gantt
|
|
649
|
+
self._gantt_box.addWidget(self._gantt)
|
|
650
|
+
|
|
651
|
+
def _sync_gantts(self) -> None:
|
|
652
|
+
"""Ensure there's a strip per CPU the kernel reports; shrink them when there are many."""
|
|
653
|
+
cpus = sorted(self.state.cpu_timelines) or [0]
|
|
654
|
+
for ci in cpus:
|
|
655
|
+
if ci not in self._gantts:
|
|
656
|
+
g = GanttStrip(self.theme, label=f"CPU {ci}")
|
|
657
|
+
self._gantts[ci] = g
|
|
658
|
+
self._gantt_box.addWidget(g)
|
|
659
|
+
multi = len(cpus) > 1
|
|
660
|
+
for ci, g in self._gantts.items():
|
|
661
|
+
g.label = f"CPU {ci}" if multi else ""
|
|
662
|
+
g.setMinimumHeight(30 if multi else 46)
|
|
663
|
+
g.setMaximumHeight(30 if multi else 46)
|
|
664
|
+
tl = self.state.cpu_timelines.get(ci)
|
|
665
|
+
g.set_slots(tl.recent() if tl else [])
|
|
666
|
+
|
|
667
|
+
def _build_launcher(self, root) -> None:
|
|
668
|
+
t = self.theme.theme
|
|
669
|
+
bar = QFrame(); bar.setStyleSheet(
|
|
670
|
+
_scss(f"QFrame{{background:{t.panel2};border:1px solid {t.line};border-radius:10px;}}"))
|
|
671
|
+
lay = QHBoxLayout(bar); lay.setContentsMargins(12, 6, 12, 6)
|
|
672
|
+
lbl = QLabel("Launch a program"); lbl.setStyleSheet(_scss(f"color:{t.muted};font-size:12px;"))
|
|
673
|
+
lay.addWidget(lbl)
|
|
674
|
+
self._prog_combo = QComboBox()
|
|
675
|
+
self._prog_combo.addItems(_LAUNCHABLE)
|
|
676
|
+
self._prog_combo.setStyleSheet(
|
|
677
|
+
f"QComboBox{{color:{t.text};background:{t.panel};border:1px solid {t.line};"
|
|
678
|
+
"border-radius:6px;padding:3px 8px;}")
|
|
679
|
+
lay.addWidget(self._prog_combo)
|
|
680
|
+
launch = QPushButton(" Launch")
|
|
681
|
+
launch.setIcon(icons.icon("play", t.accent_for("green"), 14))
|
|
682
|
+
launch.setStyleSheet(self._btn_css())
|
|
683
|
+
launch.clicked.connect(self._launch)
|
|
684
|
+
lay.addWidget(launch)
|
|
685
|
+
hint = QLabel("spin = CPU loop · alloc = grows memory · writer = file writes. "
|
|
686
|
+
"Use ✕ in the table to kill one.")
|
|
687
|
+
hint.setStyleSheet(_scss(f"color:{t.faint};font-size:11px;"))
|
|
688
|
+
lay.addWidget(hint); lay.addStretch(1)
|
|
689
|
+
root.addWidget(bar)
|
|
690
|
+
|
|
691
|
+
def _build_sched_controls(self, root) -> None:
|
|
692
|
+
"""Per-process priority + ticket setters (control-plane) — so priority/lottery have real
|
|
693
|
+
differences to schedule on. Pick a pid, set its priority (lower = higher) and lottery
|
|
694
|
+
tickets, then Set."""
|
|
695
|
+
t = self.theme.theme
|
|
696
|
+
bar = QFrame(); bar.setStyleSheet(
|
|
697
|
+
_scss(f"QFrame{{background:{t.panel2};border:1px solid {t.line};border-radius:10px;}}"))
|
|
698
|
+
lay = QHBoxLayout(bar); lay.setContentsMargins(12, 6, 12, 6)
|
|
699
|
+
lay.addWidget(QLabel("Scheduling for pid"))
|
|
700
|
+
self._sc_pid = QComboBox(); self._sc_pid.setMinimumWidth(64)
|
|
701
|
+
self._sc_pid.setStyleSheet(
|
|
702
|
+
f"QComboBox{{color:{t.text};background:{t.panel};border:1px solid {t.line};"
|
|
703
|
+
"border-radius:6px;padding:3px 8px;}")
|
|
704
|
+
self._sc_pid.currentIndexChanged.connect(self._load_sched_control)
|
|
705
|
+
lay.addWidget(self._sc_pid)
|
|
706
|
+
lay.addWidget(QLabel("priority"))
|
|
707
|
+
self._sc_prio = QSpinBox(); self._sc_prio.setRange(0, 30); self._sc_prio.setValue(10)
|
|
708
|
+
self._sc_prio.setToolTip("lower = higher priority")
|
|
709
|
+
lay.addWidget(self._sc_prio)
|
|
710
|
+
lay.addWidget(QLabel("tickets"))
|
|
711
|
+
self._sc_tickets = QSpinBox(); self._sc_tickets.setRange(1, 100); self._sc_tickets.setValue(1)
|
|
712
|
+
self._sc_tickets.setToolTip("lottery weight")
|
|
713
|
+
lay.addWidget(self._sc_tickets)
|
|
714
|
+
setb = QPushButton("Set"); setb.setStyleSheet(self._btn_css())
|
|
715
|
+
setb.clicked.connect(self._apply_sched_control)
|
|
716
|
+
lay.addWidget(setb)
|
|
717
|
+
for w in (self._sc_pid, self._sc_prio, self._sc_tickets):
|
|
718
|
+
pass
|
|
719
|
+
for lbl in bar.findChildren(QLabel):
|
|
720
|
+
lbl.setStyleSheet(_scss(f"color:{t.muted};font-size:12px;"))
|
|
721
|
+
lay.addStretch(1)
|
|
722
|
+
root.addWidget(bar)
|
|
723
|
+
|
|
724
|
+
def _sched_user_pids(self) -> list:
|
|
725
|
+
snap = self.state.latest
|
|
726
|
+
return [p for p in (snap.procs if snap else []) if p.pid > 2]
|
|
727
|
+
|
|
728
|
+
def _refresh_sched_pids(self) -> None:
|
|
729
|
+
"""Keep the pid dropdown in step with the live process list (preserve the selection)."""
|
|
730
|
+
if not hasattr(self, "_sc_pid"):
|
|
731
|
+
return
|
|
732
|
+
cur = self._sc_pid.currentData()
|
|
733
|
+
procs = self._sched_user_pids()
|
|
734
|
+
wanted = [(f"{p.pid} {p.name}", p.pid) for p in procs]
|
|
735
|
+
have = [(self._sc_pid.itemText(i), self._sc_pid.itemData(i))
|
|
736
|
+
for i in range(self._sc_pid.count())]
|
|
737
|
+
if have == wanted:
|
|
738
|
+
return
|
|
739
|
+
self._sc_pid.blockSignals(True)
|
|
740
|
+
self._sc_pid.clear()
|
|
741
|
+
for text, pid in wanted:
|
|
742
|
+
self._sc_pid.addItem(text, pid)
|
|
743
|
+
if cur is not None: # keep pointing at the same pid if it's still there
|
|
744
|
+
idx = self._sc_pid.findData(cur)
|
|
745
|
+
if idx >= 0:
|
|
746
|
+
self._sc_pid.setCurrentIndex(idx)
|
|
747
|
+
self._sc_pid.blockSignals(False)
|
|
748
|
+
self._load_sched_control()
|
|
749
|
+
|
|
750
|
+
def _load_sched_control(self) -> None:
|
|
751
|
+
"""Load the selected pid's current priority/tickets into the spinboxes."""
|
|
752
|
+
pid = self._sc_pid.currentData()
|
|
753
|
+
for p in self._sched_user_pids():
|
|
754
|
+
if p.pid == pid:
|
|
755
|
+
if p.priority is not None:
|
|
756
|
+
self._sc_prio.setValue(p.priority)
|
|
757
|
+
if p.tickets is not None:
|
|
758
|
+
self._sc_tickets.setValue(p.tickets)
|
|
759
|
+
return
|
|
760
|
+
|
|
761
|
+
def _apply_sched_control(self) -> None:
|
|
762
|
+
pid = self._sc_pid.currentData()
|
|
763
|
+
if pid is None:
|
|
764
|
+
return
|
|
765
|
+
prio, tk = self._sc_prio.value(), self._sc_tickets.value()
|
|
766
|
+
self._bg(lambda: (self.state.provider.set_priority(pid, prio),
|
|
767
|
+
self.state.provider.set_tickets(pid, tk)))
|
|
768
|
+
|
|
769
|
+
def _update_slice_lbl(self) -> None:
|
|
770
|
+
v = self._slice.value()
|
|
771
|
+
self._slice_lbl.setText(f"{v} tick{'s' if v != 1 else ''} (~{v * 0.5:.1f}s slice)")
|
|
772
|
+
|
|
773
|
+
_REG_ROWS = ["pc", "sp", "ra", "s0", "satp", "a0", "a7"] # s0 = frame pointer (backtrace lab)
|
|
774
|
+
_MEM_ROWS = ["page table (satp)", "user pc", "stack ptr", "address space"]
|
|
775
|
+
|
|
776
|
+
# -- the four state panels -------------------------------------------- #
|
|
777
|
+
def _build_panels(self, root) -> None:
|
|
778
|
+
grid = QGridLayout(); grid.setSpacing(10)
|
|
779
|
+
from .process_tree import ProcessTree
|
|
780
|
+
self._proc_tree = ProcessTree(self.theme)
|
|
781
|
+
self._proc_tree.set_live(self.live)
|
|
782
|
+
self._proc_tree.kill_requested.connect(self._kill)
|
|
783
|
+
grid.addWidget(self._panel("Processes · tree", self._proc_tree, fill=True), 0, 0)
|
|
784
|
+
# per-CPU tables: one column per CPU (so both cores' registers/memory show on SMP)
|
|
785
|
+
self._reg_tbl = self._make_cpu_table(self._REG_ROWS)
|
|
786
|
+
grid.addWidget(self._panel("CPU registers · per core", self._reg_tbl, fill=True), 0, 1)
|
|
787
|
+
# the scheduler flagship: ready queue + CPU share (the per-core memory view lives behind
|
|
788
|
+
# the Memory card on the hub, so it doesn't need a duplicate mini-panel here).
|
|
789
|
+
self._sched_panel = SchedulingPanel(self.theme)
|
|
790
|
+
grid.addWidget(self._panel("Scheduling · ready queue & CPU share", self._sched_panel,
|
|
791
|
+
fill=True), 1, 0)
|
|
792
|
+
self._stack_lbl = QLabel(); self._stack_lbl.setAlignment(Qt.AlignTop)
|
|
793
|
+
self._stack_lbl.setTextFormat(Qt.RichText)
|
|
794
|
+
t = self.theme.theme
|
|
795
|
+
self._stack_lbl.setStyleSheet(_scss(f"color:{t.text};font-family:monospace;font-size:12px;"))
|
|
796
|
+
grid.addWidget(self._panel("Kernel stack · bt (Step)", self._stack_lbl), 1, 1)
|
|
797
|
+
grid.setColumnStretch(0, 1); grid.setColumnStretch(1, 1)
|
|
798
|
+
root.addLayout(grid, 1)
|
|
799
|
+
|
|
800
|
+
def _make_cpu_table(self, rows) -> QTableWidget:
|
|
801
|
+
t = self.theme.theme
|
|
802
|
+
tbl = QTableWidget(len(rows), 0)
|
|
803
|
+
tbl.setVerticalHeaderLabels(rows)
|
|
804
|
+
tbl.setEditTriggers(QTableWidget.NoEditTriggers)
|
|
805
|
+
tbl.setSelectionMode(QTableWidget.NoSelection)
|
|
806
|
+
tbl.horizontalHeader().setStretchLastSection(True)
|
|
807
|
+
tbl.setStyleSheet(
|
|
808
|
+
f"QTableWidget{{background:{t.panel};color:{t.text};border:none;"
|
|
809
|
+
f"gridline-color:{t.line};font-family:monospace;font-size:11px;}}"
|
|
810
|
+
f"QHeaderView::section{{background:{t.panel2};color:{t.muted};border:none;"
|
|
811
|
+
"padding:3px;font-family:sans-serif;}")
|
|
812
|
+
return tbl
|
|
813
|
+
|
|
814
|
+
def _panel(self, title, inner, fill=False) -> QFrame:
|
|
815
|
+
t = self.theme.theme
|
|
816
|
+
f = QFrame(); f.setStyleSheet(
|
|
817
|
+
_scss(f"QFrame{{background:{t.panel2};border:1px solid {t.line};border-radius:10px;}}"))
|
|
818
|
+
v = QVBoxLayout(f); v.setContentsMargins(10, 8, 10, 10)
|
|
819
|
+
h = QLabel(title); h.setStyleSheet(
|
|
820
|
+
_scss(f"color:{t.muted};font-size:11px;font-weight:600;border:none;"))
|
|
821
|
+
v.addWidget(h)
|
|
822
|
+
inner.setStyleSheet((inner.styleSheet() or "") + "border:none;")
|
|
823
|
+
v.addWidget(inner, 1 if fill else 0)
|
|
824
|
+
if not fill: # keep content pinned to the top, not centered
|
|
825
|
+
v.addStretch(1)
|
|
826
|
+
return f
|
|
827
|
+
|
|
828
|
+
def _make_proc_table(self) -> QTableWidget:
|
|
829
|
+
t = self.theme.theme
|
|
830
|
+
tbl = QTableWidget(0, 4)
|
|
831
|
+
tbl.setHorizontalHeaderLabels(["PID", "State", "Name", ""])
|
|
832
|
+
tbl.verticalHeader().setVisible(False)
|
|
833
|
+
tbl.setEditTriggers(QTableWidget.NoEditTriggers)
|
|
834
|
+
tbl.setSelectionMode(QTableWidget.NoSelection)
|
|
835
|
+
tbl.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch)
|
|
836
|
+
tbl.setColumnWidth(3, 44)
|
|
837
|
+
tbl.setStyleSheet(
|
|
838
|
+
f"QTableWidget{{background:{t.panel};color:{t.text};border:none;"
|
|
839
|
+
f"gridline-color:{t.line};font-size:12px;}}"
|
|
840
|
+
f"QHeaderView::section{{background:{t.panel2};color:{t.muted};border:none;"
|
|
841
|
+
"padding:4px;}")
|
|
842
|
+
return tbl
|
|
843
|
+
|
|
844
|
+
def _make_kv_panel(self, keys) -> tuple[dict, QWidget]:
|
|
845
|
+
t = self.theme.theme
|
|
846
|
+
w = QWidget(); g = QGridLayout(w); g.setContentsMargins(0, 4, 0, 0); g.setSpacing(4)
|
|
847
|
+
fields = {}
|
|
848
|
+
for i, k in enumerate(keys):
|
|
849
|
+
kl = QLabel(k); kl.setStyleSheet(_scss(f"color:{t.muted};font-size:11px;border:none;"))
|
|
850
|
+
vl = QLabel("—"); vl.setStyleSheet(
|
|
851
|
+
_scss(f"color:{t.text};font-family:monospace;font-size:12px;border:none;"))
|
|
852
|
+
vl.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
|
853
|
+
g.addWidget(kl, i, 0); g.addWidget(vl, i, 1)
|
|
854
|
+
fields[k] = vl
|
|
855
|
+
g.setColumnStretch(1, 1)
|
|
856
|
+
return fields, w
|
|
857
|
+
|
|
858
|
+
# -- programs: launch / kill (all off the GUI thread) ----------------- #
|
|
859
|
+
def _bg(self, fn) -> None:
|
|
860
|
+
import threading
|
|
861
|
+
threading.Thread(target=fn, daemon=True).start()
|
|
862
|
+
|
|
863
|
+
def _launch(self) -> None:
|
|
864
|
+
prog = self._prog_combo.currentText()
|
|
865
|
+
self._bg(lambda: self._act_then_refresh(lambda: self.state.provider.run(prog)))
|
|
866
|
+
|
|
867
|
+
def _kill(self, pid: int) -> None:
|
|
868
|
+
self._bg(lambda: self._act_then_refresh(lambda: self.state.provider.kill(pid)))
|
|
869
|
+
|
|
870
|
+
def _act_then_refresh(self, action) -> None:
|
|
871
|
+
"""Run a serial action (launch/kill), then RE-READ the kernel several times (SEQUENTIALLY,
|
|
872
|
+
each read blocking to completion in this worker thread) so the new/removed process shows
|
|
873
|
+
up — the shell needs a beat to fork+exec (or reap). Sequential (not busy-guarded) so every
|
|
874
|
+
attempt actually fires; empty reads are ignored by MachineState, so nothing blanks."""
|
|
875
|
+
import time
|
|
876
|
+
try:
|
|
877
|
+
action()
|
|
878
|
+
except Exception:
|
|
879
|
+
pass
|
|
880
|
+
for _ in range(6):
|
|
881
|
+
if self._closed:
|
|
882
|
+
return
|
|
883
|
+
time.sleep(0.4)
|
|
884
|
+
try:
|
|
885
|
+
self.state.refresh() # blocks until the read completes (worker thread)
|
|
886
|
+
if not self._closed:
|
|
887
|
+
self.snap_ready.emit(None) # repaint on the GUI thread
|
|
888
|
+
except (Exception, RuntimeError):
|
|
889
|
+
return # dialog closed mid-refresh -> stop quietly
|
|
890
|
+
|
|
891
|
+
def _open_console(self) -> None:
|
|
892
|
+
if self.live and hasattr(self.state.provider, "console"):
|
|
893
|
+
self._console = Xv6Console(self, self.theme, self.state.provider, self.device)
|
|
894
|
+
self._console.show(); self._console.raise_()
|
|
895
|
+
elif self.on_console:
|
|
896
|
+
self.on_console()
|
|
897
|
+
else:
|
|
898
|
+
# no live console (offline demo) — the process tree IS the list of programs
|
|
899
|
+
self._show_scheduler()
|
|
900
|
+
|
|
901
|
+
# -- control handlers ------------------------------------------------- #
|
|
902
|
+
def _apply_slice(self) -> None:
|
|
903
|
+
"""Commit the time-slice ONCE, when the slider is released — off the GUI thread for the
|
|
904
|
+
live bridge (the write halts the kernel via gdb, so we must not do it inline or on every
|
|
905
|
+
drag tick)."""
|
|
906
|
+
v = self._slice.value()
|
|
907
|
+
self._update_slice_lbl()
|
|
908
|
+
if self.live:
|
|
909
|
+
self._bg(lambda: self.state.set_timeslice(v))
|
|
910
|
+
else:
|
|
911
|
+
self.state.set_timeslice(v)
|
|
912
|
+
|
|
913
|
+
def _apply_policy(self, name) -> None:
|
|
914
|
+
"""Switch the scheduler policy live. For the live bridge the write goes over the serial
|
|
915
|
+
off the GUI thread; offline it re-picks against the demo so the change is visible at once."""
|
|
916
|
+
if self.live:
|
|
917
|
+
self._bg(lambda: self.state.set_policy(name))
|
|
918
|
+
else:
|
|
919
|
+
self.state.set_policy(name)
|
|
920
|
+
self._render()
|
|
921
|
+
if hasattr(self, "_shadow_status"):
|
|
922
|
+
self._update_shadow_bar() # the bar tracks the CURRENT policy's shadow
|
|
923
|
+
|
|
924
|
+
def _sync_policy_combo(self) -> None:
|
|
925
|
+
"""Populate the policy selector from the kernel's live roster (POLICY lines) so a policy the
|
|
926
|
+
kernel ships auto-appears — no hardcoded list. Offline / no roster: keep the built-in list."""
|
|
927
|
+
combo = getattr(self, "_policy_combo", None)
|
|
928
|
+
if combo is None or not self.live:
|
|
929
|
+
return
|
|
930
|
+
roster = self.state.policies()
|
|
931
|
+
if not roster:
|
|
932
|
+
return
|
|
933
|
+
names = [roster[i] for i in sorted(roster)]
|
|
934
|
+
if names == [combo.itemText(i) for i in range(combo.count())]:
|
|
935
|
+
return # already in sync
|
|
936
|
+
cur = combo.currentText()
|
|
937
|
+
combo.blockSignals(True) # repopulate without firing _apply_policy
|
|
938
|
+
combo.clear(); combo.addItems(names)
|
|
939
|
+
if cur in names:
|
|
940
|
+
combo.setCurrentText(cur)
|
|
941
|
+
combo.blockSignals(False)
|
|
942
|
+
|
|
943
|
+
# -- the shadow bar: toggle + Load/Revert + inline result (live only) --------- #
|
|
944
|
+
_POLICY_SHADOW = {"round-robin": "rr_sched", "priority": "prio_sched",
|
|
945
|
+
"lottery": "lottery_sched"}
|
|
946
|
+
|
|
947
|
+
def _build_shadow_bar(self, root) -> None:
|
|
948
|
+
t = self.theme.theme
|
|
949
|
+
bar = QFrame(); bar.setStyleSheet(
|
|
950
|
+
_scss(f"QFrame{{background:{t.panel2};border:1px solid {t.line};border-radius:10px;}}"))
|
|
951
|
+
lay = QVBoxLayout(bar); lay.setContentsMargins(12, 8, 12, 8); lay.setSpacing(6)
|
|
952
|
+
row = QHBoxLayout()
|
|
953
|
+
lbl = QLabel("Shadow"); lbl.setStyleSheet(_scss(f"color:{t.muted};font-size:12px;"))
|
|
954
|
+
row.addWidget(lbl)
|
|
955
|
+
self._shadow_status = QLabel("—")
|
|
956
|
+
self._shadow_status.setStyleSheet(
|
|
957
|
+
_scss(f"color:{t.text};font-size:12px;font-family:monospace;"))
|
|
958
|
+
row.addWidget(self._shadow_status); row.addStretch(1)
|
|
959
|
+
self._shadow_toggle = QCheckBox("Use my shadow")
|
|
960
|
+
self._shadow_toggle.setStyleSheet(_scss(f"color:{t.text};font-size:12px;"))
|
|
961
|
+
self._shadow_toggle.toggled.connect(self._toggle_shadow)
|
|
962
|
+
row.addWidget(self._shadow_toggle)
|
|
963
|
+
self._load_btn = QPushButton(" Load")
|
|
964
|
+
self._load_btn.setIcon(icons.icon("compile", t.accent_for("green"), 14))
|
|
965
|
+
self._load_btn.setToolTip("Rebuild the kernel with your shadow and restart")
|
|
966
|
+
self._load_btn.clicked.connect(self._load_shadow)
|
|
967
|
+
self._load_btn.setStyleSheet(self._btn_css())
|
|
968
|
+
row.addWidget(self._load_btn)
|
|
969
|
+
self._revert_btn = QPushButton(" Revert")
|
|
970
|
+
self._revert_btn.setToolTip("Restore the shipped shadow and rebuild")
|
|
971
|
+
self._revert_btn.clicked.connect(self._revert_shadow)
|
|
972
|
+
self._revert_btn.setStyleSheet(self._btn_css())
|
|
973
|
+
row.addWidget(self._revert_btn)
|
|
974
|
+
lay.addLayout(row)
|
|
975
|
+
self._shadow_result = QPlainTextEdit(); self._shadow_result.setReadOnly(True)
|
|
976
|
+
self._shadow_result.setVisible(False)
|
|
977
|
+
lay.addWidget(self._shadow_result)
|
|
978
|
+
root.addWidget(bar)
|
|
979
|
+
|
|
980
|
+
def _current_shadow_name(self) -> str:
|
|
981
|
+
combo = getattr(self, "_policy_combo", None)
|
|
982
|
+
pol = combo.currentText() if combo else "priority"
|
|
983
|
+
return self._POLICY_SHADOW.get(pol, "prio_sched")
|
|
984
|
+
|
|
985
|
+
def _refresh_shadows(self) -> None:
|
|
986
|
+
if not self.live:
|
|
987
|
+
return
|
|
988
|
+
self._bg(self._fetch_shadows)
|
|
989
|
+
|
|
990
|
+
def _fetch_shadows(self) -> None:
|
|
991
|
+
try:
|
|
992
|
+
sh = self.state.shadows()
|
|
993
|
+
except (Exception, RuntimeError):
|
|
994
|
+
return
|
|
995
|
+
if not self._closed:
|
|
996
|
+
self.shadows_ready.emit(sh)
|
|
997
|
+
|
|
998
|
+
def _on_shadow_poll(self) -> None:
|
|
999
|
+
if self.live and not self._closed:
|
|
1000
|
+
self._refresh_shadows()
|
|
1001
|
+
|
|
1002
|
+
def _on_shadows(self, sh) -> None:
|
|
1003
|
+
if self._closed:
|
|
1004
|
+
return
|
|
1005
|
+
self._shadows = sh or {}
|
|
1006
|
+
self._update_shadow_bar()
|
|
1007
|
+
# reconcile a pending "run my shadow after Load": the rebuilt kernel booted on the primary,
|
|
1008
|
+
# so once its manifest is readable again, re-enable the shadow; confirm only when it's active.
|
|
1009
|
+
name = self._reenable_shadow
|
|
1010
|
+
if name:
|
|
1011
|
+
s = self._shadows.get(name)
|
|
1012
|
+
if s is None:
|
|
1013
|
+
return # kernel still booting — retry next poll
|
|
1014
|
+
if s.active:
|
|
1015
|
+
self._reenable_shadow = None
|
|
1016
|
+
self._show_result("✓ Running your shadow", "green")
|
|
1017
|
+
self._log("info", "xv6: your shadow is now running")
|
|
1018
|
+
elif not s.enabled:
|
|
1019
|
+
self._bg(lambda n=name: self._provider_call("set_shadow", n, True))
|
|
1020
|
+
|
|
1021
|
+
def _update_shadow_bar(self) -> None:
|
|
1022
|
+
if not hasattr(self, "_shadow_status"):
|
|
1023
|
+
return
|
|
1024
|
+
t = self.theme.theme
|
|
1025
|
+
name = self._current_shadow_name()
|
|
1026
|
+
s = self._shadows.get(name)
|
|
1027
|
+
if s is None:
|
|
1028
|
+
self._shadow_status.setText(f"{name}: (not reported)")
|
|
1029
|
+
else:
|
|
1030
|
+
if s.active:
|
|
1031
|
+
txt, col = f"{name}: active ✓", t.success
|
|
1032
|
+
elif s.faults:
|
|
1033
|
+
txt, col = f"{name}: faulted {s.faults}×", t.accent_for("red")
|
|
1034
|
+
elif s.enabled:
|
|
1035
|
+
txt, col = f"{name}: on (stub → primary)", t.muted
|
|
1036
|
+
else:
|
|
1037
|
+
txt, col = f"{name}: off (primary running)", t.muted
|
|
1038
|
+
if s.hash and s.hash != "baseline":
|
|
1039
|
+
txt += f" [{s.hash}]"
|
|
1040
|
+
self._shadow_status.setText(txt)
|
|
1041
|
+
self._shadow_status.setStyleSheet(
|
|
1042
|
+
_scss(f"color:{col};font-size:12px;font-family:monospace;"))
|
|
1043
|
+
self._shadow_toggle.blockSignals(True)
|
|
1044
|
+
self._shadow_toggle.setChecked(bool(s.enabled))
|
|
1045
|
+
self._shadow_toggle.blockSignals(False)
|
|
1046
|
+
# "shadow detected" — one-shot console line when the file first differs from the stub
|
|
1047
|
+
present = any(getattr(v, "is_student", False) for v in self._shadows.values())
|
|
1048
|
+
if present and not self._shadow_present_seen:
|
|
1049
|
+
self._log("info", "xv6 shadow detected — gini_sched.c edited (Load to run it)")
|
|
1050
|
+
self._shadow_present_seen = present
|
|
1051
|
+
|
|
1052
|
+
def _toggle_shadow(self, on) -> None:
|
|
1053
|
+
name = self._current_shadow_name()
|
|
1054
|
+
self._bg(lambda: self._provider_call("set_shadow", name, bool(on)))
|
|
1055
|
+
self._refresh_shadows()
|
|
1056
|
+
|
|
1057
|
+
def _provider_call(self, method, *args) -> None:
|
|
1058
|
+
fn = getattr(self.state.provider, method, None)
|
|
1059
|
+
if callable(fn):
|
|
1060
|
+
try:
|
|
1061
|
+
fn(*args)
|
|
1062
|
+
except Exception:
|
|
1063
|
+
pass
|
|
1064
|
+
|
|
1065
|
+
def _load_shadow(self) -> None:
|
|
1066
|
+
self._run_build("load", "Building your shadow…")
|
|
1067
|
+
|
|
1068
|
+
def _revert_shadow(self) -> None:
|
|
1069
|
+
self._run_build("revert", "Reverting to the shipped shadow…")
|
|
1070
|
+
|
|
1071
|
+
def _run_build(self, action, busy_msg) -> None:
|
|
1072
|
+
self._load_btn.setEnabled(False); self._revert_btn.setEnabled(False)
|
|
1073
|
+
self._show_result(busy_msg, "muted")
|
|
1074
|
+
|
|
1075
|
+
def work():
|
|
1076
|
+
fn = getattr(self.state.provider, action, None)
|
|
1077
|
+
ok, log = (fn() if callable(fn) else (False, "no live kernel"))
|
|
1078
|
+
if not self._closed:
|
|
1079
|
+
self.load_result.emit(bool(ok), str(log), action)
|
|
1080
|
+
self._bg(work)
|
|
1081
|
+
|
|
1082
|
+
def _on_load_result(self, ok, log, action) -> None:
|
|
1083
|
+
if self._closed:
|
|
1084
|
+
return
|
|
1085
|
+
self._load_btn.setEnabled(True); self._revert_btn.setEnabled(True)
|
|
1086
|
+
if not ok:
|
|
1087
|
+
self._show_result(log or "build failed", "red")
|
|
1088
|
+
self._log("error", f"xv6 shadow {action} failed — compile error (see Machine Lab)")
|
|
1089
|
+
self._reenable_shadow = None
|
|
1090
|
+
elif action == "revert":
|
|
1091
|
+
# the shipped stub is back; a rebooted kernel boots on the primary
|
|
1092
|
+
self._show_result("✓ Reverted — shipped shadow restored (primary running)", "green")
|
|
1093
|
+
self._log("info", "xv6: reverted to the shipped shadow")
|
|
1094
|
+
self._reenable_shadow = None
|
|
1095
|
+
else: # load — the kernel is rebuilt, but a fresh boot starts on the PRIMARY (shadow off)
|
|
1096
|
+
if self._shadow_toggle.isChecked():
|
|
1097
|
+
# honour the intent: re-run the shadow once the rebooted kernel is back (reconciled
|
|
1098
|
+
# in _on_shadows). Don't claim it's running until it actually is.
|
|
1099
|
+
self._reenable_shadow = self._current_shadow_name()
|
|
1100
|
+
self._show_result("✓ Loaded — bringing up your shadow…", "muted")
|
|
1101
|
+
else:
|
|
1102
|
+
self._reenable_shadow = None
|
|
1103
|
+
self._show_result("✓ Loaded — kernel rebuilt. Tick “Use my shadow” to run it.",
|
|
1104
|
+
"green")
|
|
1105
|
+
self._log("info", "xv6: loaded a rebuilt kernel")
|
|
1106
|
+
if self.live:
|
|
1107
|
+
self._fetch(step=False) # QEMU restarted -> read fresh state
|
|
1108
|
+
self._refresh_shadows()
|
|
1109
|
+
|
|
1110
|
+
def _show_result(self, text, tone) -> None:
|
|
1111
|
+
t = self.theme.theme
|
|
1112
|
+
col = {"green": t.success, "red": t.accent_for("red"),
|
|
1113
|
+
"muted": t.muted}.get(tone, t.text)
|
|
1114
|
+
self._shadow_result.setPlainText(text)
|
|
1115
|
+
self._shadow_result.setVisible(True)
|
|
1116
|
+
multiline = ("\n" in text) or tone == "red"
|
|
1117
|
+
self._shadow_result.setFixedHeight(130 if multiline else 30)
|
|
1118
|
+
self._shadow_result.setStyleSheet(
|
|
1119
|
+
f"QPlainTextEdit{{background:{t.panel};color:{col};border:1px solid {t.line};"
|
|
1120
|
+
"border-radius:6px;font-family:monospace;font-size:11px;padding:4px;}")
|
|
1121
|
+
|
|
1122
|
+
def _log(self, level, msg) -> None:
|
|
1123
|
+
if self.on_log:
|
|
1124
|
+
try:
|
|
1125
|
+
self.on_log(level, msg)
|
|
1126
|
+
except Exception:
|
|
1127
|
+
pass
|
|
1128
|
+
|
|
1129
|
+
def _on_poll(self) -> None:
|
|
1130
|
+
"""The Run loop = OBSERVE the free-running kernel: just read current state on a timer.
|
|
1131
|
+
Crucially NOT step() — on a live idle kernel, stepping waits for a context switch that
|
|
1132
|
+
never comes and would hang. The demo has no real kernel, so it advances its round-robin
|
|
1133
|
+
to animate."""
|
|
1134
|
+
if self.live:
|
|
1135
|
+
self._fetch(step=False) # refresh (read), never halt-and-wait
|
|
1136
|
+
else:
|
|
1137
|
+
try:
|
|
1138
|
+
self.state.step() # demo: animate the fake round-robin
|
|
1139
|
+
except Exception:
|
|
1140
|
+
return
|
|
1141
|
+
self._render()
|
|
1142
|
+
|
|
1143
|
+
def _on_step(self) -> None:
|
|
1144
|
+
"""The manual 'Step switch' button = advance exactly one context switch (halts on
|
|
1145
|
+
swtch). On a live idle kernel this may time out harmlessly (async, so no freeze)."""
|
|
1146
|
+
if self.live:
|
|
1147
|
+
self._fetch(step=True) # live: read gdb off the GUI thread
|
|
1148
|
+
return
|
|
1149
|
+
try:
|
|
1150
|
+
self.state.step() # demo: instant, run inline
|
|
1151
|
+
except Exception:
|
|
1152
|
+
return
|
|
1153
|
+
self._render()
|
|
1154
|
+
|
|
1155
|
+
def _fetch(self, step: bool) -> None:
|
|
1156
|
+
"""Pull a snapshot from the live bridge on a worker thread (gdb-over-HTTP can block for
|
|
1157
|
+
a moment or time out), then repaint on the GUI thread. Skips if a read is in flight so
|
|
1158
|
+
Run/Step can't pile up and freeze the UI."""
|
|
1159
|
+
if self._busy or self._closed:
|
|
1160
|
+
return
|
|
1161
|
+
self._busy = True
|
|
1162
|
+
import threading
|
|
1163
|
+
|
|
1164
|
+
def work():
|
|
1165
|
+
try:
|
|
1166
|
+
self.state.step() if step else self.state.refresh()
|
|
1167
|
+
if not self._closed: # don't signal a dialog that's being torn down
|
|
1168
|
+
self.snap_ready.emit(None) # marshal back to the GUI thread
|
|
1169
|
+
except (Exception, RuntimeError):
|
|
1170
|
+
pass # incl. dialog closed mid-read
|
|
1171
|
+
threading.Thread(target=work, daemon=True).start()
|
|
1172
|
+
|
|
1173
|
+
def _on_snap(self, _obj) -> None:
|
|
1174
|
+
self._busy = False
|
|
1175
|
+
if self._closed:
|
|
1176
|
+
return
|
|
1177
|
+
self._render()
|
|
1178
|
+
|
|
1179
|
+
def _toggle_run(self) -> None:
|
|
1180
|
+
t = self.theme.theme
|
|
1181
|
+
self._running = not self._running
|
|
1182
|
+
if self._running:
|
|
1183
|
+
self._run_btn.setText(" Pause")
|
|
1184
|
+
self._run_btn.setIcon(icons.icon("stop", t.accent_for("amber"), 15))
|
|
1185
|
+
self._poll.start(500 if self.live else 700) # fast procdump reads -> sample often
|
|
1186
|
+
else:
|
|
1187
|
+
self._run_btn.setText(" Run")
|
|
1188
|
+
self._run_btn.setIcon(icons.icon("play", t.accent_for("green"), 15))
|
|
1189
|
+
self._poll.stop()
|
|
1190
|
+
|
|
1191
|
+
# -- render from the shared state ------------------------------------- #
|
|
1192
|
+
def _render(self, *_a) -> None:
|
|
1193
|
+
self._update_banner()
|
|
1194
|
+
snap = self.state.latest
|
|
1195
|
+
if snap is None:
|
|
1196
|
+
self._update_overview() # keep the hub's mini-stats honest (blank) with no data
|
|
1197
|
+
return
|
|
1198
|
+
t = self.theme.theme
|
|
1199
|
+
# process TREE — highlight whatever is running on any CPU (SMP-aware)
|
|
1200
|
+
run = snap.running_pid
|
|
1201
|
+
running_pids = set(snap.cpus.values()) if snap.cpus else ({run} if run else set())
|
|
1202
|
+
# scheduling badges: mark starving / CPU-monopolising procs in the tree
|
|
1203
|
+
sf = self.state.scheduling_flags()
|
|
1204
|
+
flags = {pid: "starving" for pid in sf.get("starvation", ())}
|
|
1205
|
+
for pid in sf.get("cpu_monopoly", ()):
|
|
1206
|
+
flags.setdefault(pid, "monopolising CPU")
|
|
1207
|
+
self._proc_tree.set_procs(snap.procs, running_pids, flags=flags)
|
|
1208
|
+
if hasattr(self, "_sc_pid"):
|
|
1209
|
+
self._refresh_sched_pids() # keep the priority/tickets pid list live
|
|
1210
|
+
# per-CPU registers (a column per core; falls back to one column single-CPU)
|
|
1211
|
+
cpu_regs = snap.cpu_regs or ({0: snap.cpu} if snap.cpu else {})
|
|
1212
|
+
cids = sorted(cpu_regs)
|
|
1213
|
+
self._reg_tbl.setColumnCount(len(cids))
|
|
1214
|
+
heads = [f"CPU {c} · pid {cpu_regs[c].key('pid')}" for c in cids]
|
|
1215
|
+
self._reg_tbl.setHorizontalHeaderLabels(heads)
|
|
1216
|
+
for col, c in enumerate(cids):
|
|
1217
|
+
cs = cpu_regs[c]
|
|
1218
|
+
for row, key in enumerate(self._REG_ROWS):
|
|
1219
|
+
self._reg_tbl.setItem(row, col, QTableWidgetItem(cs.key(key)))
|
|
1220
|
+
# scheduling panel: ready queue + CPU share (share from the aggregate timeline)
|
|
1221
|
+
self._sched_panel.update_view(snap.procs, self.state.timeline.shares())
|
|
1222
|
+
# kernel stack (from gdb on Step; user procs are in user mode during Run)
|
|
1223
|
+
if snap.stack:
|
|
1224
|
+
rows = "<br>".join(
|
|
1225
|
+
f"<span style='color:{t.muted}'>#{i}</span> {f.fn}"
|
|
1226
|
+
+ (f" <span style='color:{t.faint}'>{f.loc}</span>" if f.loc else "")
|
|
1227
|
+
for i, f in enumerate(snap.stack))
|
|
1228
|
+
self._stack_lbl.setText(rows)
|
|
1229
|
+
else:
|
|
1230
|
+
self._stack_lbl.setText(
|
|
1231
|
+
f"<span style='color:{t.faint}'>Press <b>Step switch</b> to capture the kernel "
|
|
1232
|
+
"backtrace at a context switch. (Running processes are in user mode.)</span>")
|
|
1233
|
+
# gantt + counter + a switches/second meter (drops as the time-slice grows -> shows the
|
|
1234
|
+
# slider actually taking effect, which coarse sampling otherwise hides)
|
|
1235
|
+
import time as _time
|
|
1236
|
+
if not hasattr(self, "_rate"):
|
|
1237
|
+
import collections
|
|
1238
|
+
self._rate = collections.deque(maxlen=40)
|
|
1239
|
+
self._rate.append((_time.monotonic(), run))
|
|
1240
|
+
_chg = sum(1 for i in range(1, len(self._rate))
|
|
1241
|
+
if self._rate[i][1] != self._rate[i - 1][1])
|
|
1242
|
+
_win = (self._rate[-1][0] - self._rate[0][0]) if len(self._rate) > 1 else 0.0
|
|
1243
|
+
self._switch_rate = (_chg / _win) if _win > 0.5 else 0.0
|
|
1244
|
+
self._sync_gantts() # per-CPU strips (SMP) from the CPU dump lines
|
|
1245
|
+
n = self.state.timeline.switches()
|
|
1246
|
+
kq = getattr(self.state.provider, "kernel_quantum", None) # the kernel's ACTUAL quantum
|
|
1247
|
+
qtxt = f" · Q{kq}" if kq else ""
|
|
1248
|
+
self._switch_lbl.setText(f"{n} sw · {self._switch_rate:.1f}/s{qtxt}")
|
|
1249
|
+
self._sync_policy_combo() # data-driven selector from the kernel roster
|
|
1250
|
+
# policy confirmation: what the kernel actually reports (live id) or the demo's policy
|
|
1251
|
+
kp = getattr(self.state.provider, "kernel_policy", None)
|
|
1252
|
+
kp_name = policy_name(kp) if kp is not None else getattr(
|
|
1253
|
+
self.state.provider, "policy", None) or self.state.policy
|
|
1254
|
+
self._policy_kernel_lbl.setText(f"kernel: {kp_name}")
|
|
1255
|
+
self._update_overview()
|
|
1256
|
+
|
|
1257
|
+
def _update_banner(self) -> None:
|
|
1258
|
+
if not hasattr(self, "_banner"):
|
|
1259
|
+
return
|
|
1260
|
+
no_data = (self.state.mode == "real" and self.state.latest is None)
|
|
1261
|
+
self._banner.setVisible(no_data)
|
|
1262
|
+
if no_data:
|
|
1263
|
+
self._banner_lbl.setText(
|
|
1264
|
+
"No live data from the xv6 machine. It doesn't look like it's running — start the "
|
|
1265
|
+
"topology (Run), or switch to Demo to explore the interface."
|
|
1266
|
+
if not self.state.has_real()
|
|
1267
|
+
else "Real mode is selected but no data has arrived yet — waiting for the xv6 "
|
|
1268
|
+
"machine…")
|
|
1269
|
+
|
|
1270
|
+
def _configured_cores(self) -> int:
|
|
1271
|
+
"""The number of cores QEMU was launched with (derived from the Size tier), so the count is
|
|
1272
|
+
correct even when cores are idle — idle cores emit no per-core dump line, which is why a
|
|
1273
|
+
live-sample would read '1 core'."""
|
|
1274
|
+
try:
|
|
1275
|
+
from ..services.compiler import _xv6_harts
|
|
1276
|
+
return max(1, _xv6_harts(self.device))
|
|
1277
|
+
except Exception:
|
|
1278
|
+
return 1
|
|
1279
|
+
|
|
1280
|
+
def _update_overview(self) -> None:
|
|
1281
|
+
"""Refresh the overview cards' live mini-stats from the (cheap) scheduler snapshot.
|
|
1282
|
+
Only the always-cheap cards carry a live number; Memory / File system / Builder keep a
|
|
1283
|
+
descriptive line so the hub never hammers the serial just to draw the front door."""
|
|
1284
|
+
cards = getattr(self, "_ov_cards", None)
|
|
1285
|
+
if not cards:
|
|
1286
|
+
return
|
|
1287
|
+
snap = self.state.latest
|
|
1288
|
+
procs = snap.procs if snap else []
|
|
1289
|
+
user = [p for p in procs if p.pid and p.pid > 2] # everything past init(1)+sh(2)
|
|
1290
|
+
active = [p for p in procs if p.state in ("running", "runnable", "sleeping")]
|
|
1291
|
+
rate = getattr(self, "_switch_rate", 0.0)
|
|
1292
|
+
ncpu = self._configured_cores() # the cores QEMU was launched with (Size), not the
|
|
1293
|
+
run = snap.running_pid if snap else None # idle live-sample (idle cores emit no dump lines)
|
|
1294
|
+
if "programs" in cards:
|
|
1295
|
+
cards["programs"].set_stat(
|
|
1296
|
+
f"{len(user)} user program{'s' if len(user) != 1 else ''}" if user
|
|
1297
|
+
else "shell ready")
|
|
1298
|
+
if "scheduler" in cards:
|
|
1299
|
+
cards["scheduler"].set_stat(f"{len(active)} procs · {rate:.1f} sw/s")
|
|
1300
|
+
if "journey" in cards:
|
|
1301
|
+
cards["journey"].set_stat(f"{self.state.timeline.switches()} switches so far")
|
|
1302
|
+
if "cpu" in cards:
|
|
1303
|
+
cards["cpu"].set_stat(
|
|
1304
|
+
f"{ncpu} core{'s' if ncpu != 1 else ''}"
|
|
1305
|
+
+ (f" · running pid {run}" if run else " · idle"))
|
|
1306
|
+
# descriptive (no cheap live number without an extra serial read)
|
|
1307
|
+
cards.get("syscalls") and cards["syscalls"].set_stat("open for live histogram →")
|
|
1308
|
+
cards.get("builder") and cards["builder"].set_stat("generate kernel edits →")
|
|
1309
|
+
cards.get("memory") and cards["memory"].set_stat("page tables & faults →")
|
|
1310
|
+
cards.get("storage") and cards["storage"].set_stat("inodes & the log →")
|
|
1311
|
+
|
|
1312
|
+
def closeEvent(self, e) -> None: # noqa: N802
|
|
1313
|
+
self._closed = True # stop any in-flight worker from signalling a dead dialog
|
|
1314
|
+
self._poll.stop()
|
|
1315
|
+
self._ov_poll.stop()
|
|
1316
|
+
self._shadow_poll.stop()
|
|
1317
|
+
try:
|
|
1318
|
+
self.snap_ready.disconnect(self._on_snap)
|
|
1319
|
+
except (RuntimeError, TypeError):
|
|
1320
|
+
pass
|
|
1321
|
+
super().closeEvent(e)
|
|
1322
|
+
|
|
1323
|
+
|
|
1324
|
+
class Xv6Console(QDialog):
|
|
1325
|
+
"""An in-app xv6 serial console. The agent owns the single QEMU serial (a second raw client
|
|
1326
|
+
would be refused), so this reads the console tail via /console and sends input via /input —
|
|
1327
|
+
all off the GUI thread."""
|
|
1328
|
+
|
|
1329
|
+
tail_ready = Signal(str)
|
|
1330
|
+
|
|
1331
|
+
def __init__(self, parent, theme: ThemeManager, provider, device=None) -> None:
|
|
1332
|
+
super().__init__(parent)
|
|
1333
|
+
self.theme = theme
|
|
1334
|
+
self.provider = provider
|
|
1335
|
+
t = theme.theme
|
|
1336
|
+
self.setWindowTitle(f"xv6 console — {getattr(device, 'name', 'xv6')}")
|
|
1337
|
+
self.resize(720, 460)
|
|
1338
|
+
self.setStyleSheet(f"QDialog{{background:{t.bg};}}")
|
|
1339
|
+
v = QVBoxLayout(self)
|
|
1340
|
+
self.view = QPlainTextEdit(); self.view.setReadOnly(True)
|
|
1341
|
+
self.view.setStyleSheet(
|
|
1342
|
+
f"QPlainTextEdit{{background:{t.panel};color:{t.text};border:1px solid {t.line};"
|
|
1343
|
+
"border-radius:6px;font-family:monospace;font-size:12px;}")
|
|
1344
|
+
v.addWidget(self.view, 1)
|
|
1345
|
+
row = QHBoxLayout()
|
|
1346
|
+
self.input = QLineEdit(); self.input.setPlaceholderText("type a command and press Enter…")
|
|
1347
|
+
self.input.setStyleSheet(
|
|
1348
|
+
f"QLineEdit{{background:{t.panel};color:{t.text};border:1px solid {t.line};"
|
|
1349
|
+
"border-radius:6px;padding:6px;font-family:monospace;}")
|
|
1350
|
+
self.input.returnPressed.connect(self._send)
|
|
1351
|
+
row.addWidget(self.input, 1)
|
|
1352
|
+
# xv6 has no `ps`; Ctrl-P dumps the process table. This button sends that control char
|
|
1353
|
+
# (awkward to type) and refreshes — a reliable manual process listing.
|
|
1354
|
+
pbtn = QPushButton(" List processes ⌃P")
|
|
1355
|
+
pbtn.setToolTip("Send Ctrl-P — xv6's process dump (its 'ps')")
|
|
1356
|
+
pbtn.setStyleSheet(
|
|
1357
|
+
f"QPushButton{{color:{t.text};background:{t.panel2};border:1px solid {t.line};"
|
|
1358
|
+
f"border-radius:6px;padding:6px 12px;}}QPushButton:hover{{border-color:{t.accent};}}")
|
|
1359
|
+
pbtn.clicked.connect(self._procdump)
|
|
1360
|
+
row.addWidget(pbtn)
|
|
1361
|
+
v.addLayout(row)
|
|
1362
|
+
hint = QLabel("xv6 has no `ps` — use ⌃P (above) to list processes.")
|
|
1363
|
+
hint.setStyleSheet(_scss(f"color:{t.faint};font-size:11px;"))
|
|
1364
|
+
v.addWidget(hint)
|
|
1365
|
+
self.tail_ready.connect(self._show)
|
|
1366
|
+
self._poll = QTimer(self); self._poll.timeout.connect(self._refresh)
|
|
1367
|
+
self._poll.start(1200)
|
|
1368
|
+
self._refresh()
|
|
1369
|
+
|
|
1370
|
+
def _bg(self, fn):
|
|
1371
|
+
import threading
|
|
1372
|
+
threading.Thread(target=fn, daemon=True).start()
|
|
1373
|
+
|
|
1374
|
+
def _procdump(self):
|
|
1375
|
+
# send Ctrl-P (xv6's process dump), then pull the console so it shows up
|
|
1376
|
+
import time
|
|
1377
|
+
|
|
1378
|
+
def work():
|
|
1379
|
+
try:
|
|
1380
|
+
self.provider.send_input("\x10")
|
|
1381
|
+
time.sleep(0.4)
|
|
1382
|
+
txt = self.provider.console()
|
|
1383
|
+
except Exception:
|
|
1384
|
+
txt = ""
|
|
1385
|
+
self.tail_ready.emit(txt or "")
|
|
1386
|
+
self._bg(work)
|
|
1387
|
+
|
|
1388
|
+
def _refresh(self):
|
|
1389
|
+
def work():
|
|
1390
|
+
try:
|
|
1391
|
+
txt = self.provider.console()
|
|
1392
|
+
except Exception:
|
|
1393
|
+
txt = ""
|
|
1394
|
+
self.tail_ready.emit(txt or "")
|
|
1395
|
+
self._bg(work)
|
|
1396
|
+
|
|
1397
|
+
def _show(self, txt):
|
|
1398
|
+
if txt and txt != self.view.toPlainText():
|
|
1399
|
+
sb = self.view.verticalScrollBar()
|
|
1400
|
+
at_bottom = sb.value() >= sb.maximum() - 4
|
|
1401
|
+
self.view.setPlainText(txt)
|
|
1402
|
+
if at_bottom:
|
|
1403
|
+
sb.setValue(sb.maximum())
|
|
1404
|
+
|
|
1405
|
+
def _send(self):
|
|
1406
|
+
line = self.input.text()
|
|
1407
|
+
self.input.clear()
|
|
1408
|
+
self._bg(lambda: self.provider.send_input(line + "\n"))
|
|
1409
|
+
|
|
1410
|
+
def closeEvent(self, e): # noqa: N802
|
|
1411
|
+
self._poll.stop()
|
|
1412
|
+
super().closeEvent(e)
|