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/domain/xv6.py
ADDED
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
"""xv6 (RISC-V) state model + parsers — the Machine Lab's read side.
|
|
2
|
+
|
|
3
|
+
State is read from a running xv6 under QEMU without patching the kernel (GDB-read-first):
|
|
4
|
+
• the process table from xv6's built-in Ctrl-P dump (`procdump`), or from GDB reading `proc[]`;
|
|
5
|
+
• CPU registers from GDB `info registers`;
|
|
6
|
+
• the kernel stack from GDB `bt`.
|
|
7
|
+
|
|
8
|
+
This module is PURE (text -> dataclasses), so the parsing is unit-tested without QEMU/GDB.
|
|
9
|
+
The GDB/console client that produces the text is a thin runtime bridge (Mac/Docker side); the
|
|
10
|
+
Machine Lab renders whatever this returns and accumulates a scheduling timeline.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
|
|
17
|
+
# xv6 procdump prints short state words (Ctrl-P); the in-container gdb agent emits full words.
|
|
18
|
+
# Accept both and normalise to full names.
|
|
19
|
+
_STATE = {"unused": "unused", "used": "used", "sleep": "sleeping", "sleeping": "sleeping",
|
|
20
|
+
"runble": "runnable", "runnable": "runnable", "run": "running", "running": "running",
|
|
21
|
+
"zombie": "zombie"}
|
|
22
|
+
_RUNNING = "running"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Proc:
|
|
27
|
+
pid: int
|
|
28
|
+
state: str # unused|used|sleeping|runnable|running|zombie
|
|
29
|
+
name: str
|
|
30
|
+
parent: int | None = None
|
|
31
|
+
cpu_ticks: int | None = None
|
|
32
|
+
priority: int | None = None # GINI sched field (lower = higher); None if the build omits it
|
|
33
|
+
tickets: int | None = None # lottery weight
|
|
34
|
+
level: int | None = None # MLFQ queue level (student policies)
|
|
35
|
+
wait_ticks: int | None = None # aging counter (slices spent RUNNABLE)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def running(self) -> bool:
|
|
39
|
+
return self.state == _RUNNING
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class CpuState:
|
|
44
|
+
regs: dict = field(default_factory=dict) # name -> value string (pc, sp, ra, satp, …)
|
|
45
|
+
|
|
46
|
+
def key(self, name: str) -> str:
|
|
47
|
+
return self.regs.get(name, "—")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class Frame:
|
|
52
|
+
fn: str
|
|
53
|
+
loc: str = "" # file:line, if known
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class Snapshot:
|
|
58
|
+
procs: list = field(default_factory=list) # [Proc]
|
|
59
|
+
running_pid: int | None = None
|
|
60
|
+
ticks: int | None = None
|
|
61
|
+
cpu: CpuState | None = None
|
|
62
|
+
stack: list = field(default_factory=list) # [Frame]
|
|
63
|
+
cpus: dict = field(default_factory=dict) # cpu_index -> running pid (SMP; {} = single CPU)
|
|
64
|
+
cpu_regs: dict = field(default_factory=dict) # cpu_index -> CpuState (per-CPU registers)
|
|
65
|
+
modetime: dict = field(default_factory=dict) # {"user","kernel","idle"} cumulative timer ticks
|
|
66
|
+
csr: dict = field(default_factory=dict) # dumping hart's control CSRs {name: int}
|
|
67
|
+
source: str = "real" # "real" (live kernel) or "demo" (DemoScheduler)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# -- parsers ---------------------------------------------------------------- #
|
|
71
|
+
# `<pid> <state> <name> [<ppid>]` — ppid is optional (stock procdump omits it; gini_dump adds it).
|
|
72
|
+
_PROC_RE = re.compile(r"^\s*(\d+)\s+([A-Za-z]+)\s+(\S+)(?:\s+(\d+))?")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_procdump(text: str) -> list[Proc]:
|
|
76
|
+
"""xv6 process dump lines `<pid> <state> <name> [<ppid>]` -> Procs (active only). The
|
|
77
|
+
optional ppid (from gini_dump) drives the process tree; stock procdump lines parse fine
|
|
78
|
+
with parent left as None."""
|
|
79
|
+
out: list[Proc] = []
|
|
80
|
+
for line in (text or "").splitlines():
|
|
81
|
+
m = _PROC_RE.match(line)
|
|
82
|
+
if not m:
|
|
83
|
+
continue
|
|
84
|
+
st = _STATE.get(m.group(2).lower())
|
|
85
|
+
if st is None or st == "unused":
|
|
86
|
+
continue
|
|
87
|
+
parent = int(m.group(4)) if m.group(4) is not None else None
|
|
88
|
+
out.append(Proc(int(m.group(1)), st, m.group(3), parent=parent))
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
_REG_RE = re.compile(r"^\s*([a-z][a-z0-9_]*)\s+(0x[0-9a-fA-F]+|-?\d+)")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def parse_registers(text: str, keep=("pc", "sp", "ra", "satp", "epc", "a0", "a7")) -> CpuState:
|
|
96
|
+
"""GDB `info registers` -> CpuState (all regs; `keep` are the ones the UI leads with)."""
|
|
97
|
+
regs: dict = {}
|
|
98
|
+
for line in (text or "").splitlines():
|
|
99
|
+
m = _REG_RE.match(line)
|
|
100
|
+
if m:
|
|
101
|
+
regs[m.group(1)] = m.group(2)
|
|
102
|
+
return CpuState(regs=regs)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
_BT_RE = re.compile(r"^#\d+\s+(?:0x[0-9a-fA-F]+\s+in\s+)?(\w+)\s*\([^)]*\)(?:\s+at\s+(\S+))?")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def parse_backtrace(text: str) -> list[Frame]:
|
|
109
|
+
"""GDB `bt` -> [Frame] (innermost first)."""
|
|
110
|
+
out: list[Frame] = []
|
|
111
|
+
for line in (text or "").splitlines():
|
|
112
|
+
m = _BT_RE.match(line.strip())
|
|
113
|
+
if m:
|
|
114
|
+
out.append(Frame(m.group(1), m.group(2) or ""))
|
|
115
|
+
return out
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def parse_regs_line(text: str) -> CpuState:
|
|
119
|
+
"""The FIRST `REGS …` line's registers (a single-CPU summary for the Ask GINI card)."""
|
|
120
|
+
for line in (text or "").splitlines():
|
|
121
|
+
if "REGS" in line:
|
|
122
|
+
return CpuState(regs=dict(re.findall(r"([a-z0-9]+)\s+(0x[0-9a-fA-F]+)", line)))
|
|
123
|
+
return CpuState(regs={})
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def parse_cpu_regs(text: str) -> dict:
|
|
127
|
+
"""Parse gini_dump's per-CPU `REGS cpu <i> pid <p> pc 0x.. sp 0x.. …` lines ->
|
|
128
|
+
{cpu_index: CpuState} — each CPU's live registers, no gdb halt."""
|
|
129
|
+
out: dict = {}
|
|
130
|
+
for m in re.finditer(r"REGS cpu (\d+) pid (\d+)([^\n]*)", text or ""):
|
|
131
|
+
regs = dict(re.findall(r"([a-z0-9]+)\s+(0x[0-9a-fA-F]+)", m.group(3)))
|
|
132
|
+
regs["pid"] = m.group(2)
|
|
133
|
+
out[int(m.group(1))] = CpuState(regs=regs)
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def parse_cpu_lines(text: str) -> dict:
|
|
138
|
+
"""Parse gini_dump's `CPU <i> pid <n>` lines -> {cpu_index: running_pid}. Empty on a
|
|
139
|
+
single-CPU kernel (or old build) — the UI then falls back to one strip for running_pid."""
|
|
140
|
+
return {int(m.group(1)): int(m.group(2))
|
|
141
|
+
for m in re.finditer(r"CPU\s+(\d+)\s+pid\s+(\d+)", text or "")}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def parse_sched(text: str) -> dict:
|
|
145
|
+
"""Parse gini_dump's `SCHED policy N quantum N` line — the kernel's ACTUAL scheduler settings
|
|
146
|
+
(so the UI can show the real quantum/policy and confirm a control took effect)."""
|
|
147
|
+
m = re.search(r"SCHED\s+policy\s+(\d+)\s+quantum\s+(\d+)", text or "")
|
|
148
|
+
return {"policy": int(m.group(1)), "quantum": int(m.group(2))} if m else {}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# scheduler policy codes must match gini_patch.py's gini_pick() (0=RR, 1=priority, 2=lottery).
|
|
152
|
+
# Custom student policies (MLFQ, stride, …) added via the Scheduler Builder extend this map.
|
|
153
|
+
POLICY_NAMES = {0: "round-robin", 1: "priority", 2: "lottery"}
|
|
154
|
+
POLICY_IDS = {v: k for k, v in POLICY_NAMES.items()}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def policy_name(num) -> str:
|
|
158
|
+
return POLICY_NAMES.get(num, f"policy{num}")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
_POLICY_ROSTER_RE = re.compile(r"POLICY\s+(\d+)\s+(\S+)")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def parse_policies(text: str) -> dict:
|
|
165
|
+
"""gini_dump's `POLICY <id> <name>` roster lines -> {id: name}. Lets the UI populate the policy
|
|
166
|
+
selector from whatever the KERNEL ships (a new policy auto-appears), instead of a hardcoded
|
|
167
|
+
list. Empty on an older build (the UI then falls back to POLICY_NAMES)."""
|
|
168
|
+
return {int(m.group(1)): m.group(2) for m in _POLICY_ROSTER_RE.finditer(text or "")}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
_PROC_SCHED_RE = re.compile(
|
|
172
|
+
r"PROC\s+(\d+)\s+pri\s+(-?\d+)\s+tk\s+(-?\d+)\s+lv\s+(-?\d+)\s+wait\s+(-?\d+)")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def parse_proc_sched(text: str) -> dict:
|
|
176
|
+
"""gini_dump's per-proc `PROC <pid> pri P tk T lv L wait W` lines ->
|
|
177
|
+
{pid: {"priority":P, "tickets":T, "level":L, "wait_ticks":W}}. Absent on an older build."""
|
|
178
|
+
out: dict = {}
|
|
179
|
+
for m in _PROC_SCHED_RE.finditer(text or ""):
|
|
180
|
+
out[int(m.group(1))] = {"priority": int(m.group(2)), "tickets": int(m.group(3)),
|
|
181
|
+
"level": int(m.group(4)), "wait_ticks": int(m.group(5))}
|
|
182
|
+
return out
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# -- mode-time + control CSRs (the CPU face) --------------------------------------------------- #
|
|
186
|
+
_MODETIME_RE = re.compile(r"MODETIME\s+user\s+(\d+)\s+kernel\s+(\d+)\s+idle\s+(\d+)")
|
|
187
|
+
_CSR_RE = re.compile(
|
|
188
|
+
r"CSR\s+sstatus\s+(0x[0-9a-fA-F]+)\s+sie\s+(0x[0-9a-fA-F]+)\s+sip\s+(0x[0-9a-fA-F]+)"
|
|
189
|
+
r"\s+stvec\s+(0x[0-9a-fA-F]+)\s+scause\s+(0x[0-9a-fA-F]+)\s+sepc\s+(0x[0-9a-fA-F]+)")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def parse_modetime(text: str) -> dict:
|
|
193
|
+
"""gini_dump's `MODETIME user U kernel K idle I` — cumulative timer-tick counts by privilege
|
|
194
|
+
source. {} on an older build. The CPU face diffs two samples to get the last-second split."""
|
|
195
|
+
m = _MODETIME_RE.search(text or "")
|
|
196
|
+
return {"user": int(m.group(1)), "kernel": int(m.group(2)), "idle": int(m.group(3))} if m else {}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def parse_csr(text: str) -> dict:
|
|
200
|
+
"""gini_dump's `CSR sstatus .. sie .. sip .. stvec .. scause .. sepc ..` -> {name: int}. These
|
|
201
|
+
are the DUMPING hart's control CSRs (read inside the trap handler). {} on an older build."""
|
|
202
|
+
m = _CSR_RE.search(text or "")
|
|
203
|
+
if not m:
|
|
204
|
+
return {}
|
|
205
|
+
keys = ("sstatus", "sie", "sip", "stvec", "scause", "sepc")
|
|
206
|
+
return {k: int(m.group(i + 1), 16) for i, k in enumerate(keys)}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def mode_split(prev: dict | None, cur: dict | None) -> dict:
|
|
210
|
+
"""Fraction of timer ticks spent in each mode between two MODETIME samples. `prev=None` (or an
|
|
211
|
+
empty/zero baseline) yields the since-boot ratio — a sane first frame before a delta exists.
|
|
212
|
+
Always sums to 1.0 (or all-zero when there's no motion yet)."""
|
|
213
|
+
cur = cur or {}
|
|
214
|
+
prev = prev or {}
|
|
215
|
+
d = {k: max(0, cur.get(k, 0) - prev.get(k, 0)) for k in ("user", "kernel", "idle")}
|
|
216
|
+
total = sum(d.values())
|
|
217
|
+
if total == 0:
|
|
218
|
+
return {"user": 0.0, "kernel": 0.0, "idle": 0.0}
|
|
219
|
+
return {k: v / total for k, v in d.items()}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# RISC-V sstatus / sie / sip bit positions (privileged spec).
|
|
223
|
+
SSTATUS_SIE, SSTATUS_SPIE, SSTATUS_SPP = 1 << 1, 1 << 5, 1 << 8
|
|
224
|
+
SSTATUS_SUM, SSTATUS_MXR = 1 << 18, 1 << 19
|
|
225
|
+
_INT_BITS = (("software", 1 << 1), ("timer", 1 << 5), ("external", 1 << 9)) # S-mode xIE/xIP
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def sstatus_flags(v: int) -> dict:
|
|
229
|
+
"""Decode sstatus into the flags a student cares about. SPP = the privilege the current trap
|
|
230
|
+
interrupted (U for a running user program, S for a kernel-path trap) — our honest 'where the
|
|
231
|
+
CPU came from', since the live mode is always S while the dump runs."""
|
|
232
|
+
return {"SIE": bool(v & SSTATUS_SIE), "SPIE": bool(v & SSTATUS_SPIE),
|
|
233
|
+
"SPP": "S" if v & SSTATUS_SPP else "U",
|
|
234
|
+
"SUM": bool(v & SSTATUS_SUM), "MXR": bool(v & SSTATUS_MXR)}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
_SCAUSE_INT = {1: "software int", 5: "timer int", 9: "external int"}
|
|
238
|
+
_SCAUSE_EXC = {0: "instruction misaligned", 2: "illegal instruction", 8: "ecall (syscall)",
|
|
239
|
+
12: "instruction page fault", 13: "load page fault", 15: "store page fault"}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def short_pid(pid: int) -> str:
|
|
243
|
+
"""Last two digits of a pid, unpadded — a compact Gantt label. Small pids print as-is
|
|
244
|
+
(6->'6', 10->'10', 11->'11'); longer ones take the last two (230->'30', 3615->'15', 3613->'13'),
|
|
245
|
+
so pids that share a middle stay distinct (a centre-clipped full pid showed '61' for both)."""
|
|
246
|
+
return str(pid % 100)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def scause_str(v: int) -> str:
|
|
250
|
+
"""Decode the last-trap cause CSR. Top bit set = interrupt, else exception; low bits = code."""
|
|
251
|
+
if v is None:
|
|
252
|
+
return "—"
|
|
253
|
+
code = v & 0xFF
|
|
254
|
+
if v >> 63:
|
|
255
|
+
return _SCAUSE_INT.get(code, f"interrupt {code}")
|
|
256
|
+
return _SCAUSE_EXC.get(code, f"exception {code}")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def interrupt_sources(sie: int, sip: int) -> list:
|
|
260
|
+
"""The three S-mode interrupt sources with their enabled (sie) + pending (sip) bits — the honest
|
|
261
|
+
'interrupt state', read from the enable CONFIG rather than the momentary global bit."""
|
|
262
|
+
return [{"name": name, "enabled": bool(sie & bit), "pending": bool(sip & bit)}
|
|
263
|
+
for name, bit in _INT_BITS]
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# -- alarm state (the sigalarm lab; gini_dump `ALARM …` lines) --------------------------------- #
|
|
267
|
+
@dataclass
|
|
268
|
+
class AlarmState:
|
|
269
|
+
pid: int
|
|
270
|
+
interval: int # alarm period in timer ticks (0 = no alarm set)
|
|
271
|
+
ticks: int # ticks elapsed since the last fire
|
|
272
|
+
handler: str = "0x0" # handler VA (hex)
|
|
273
|
+
on: int = 0 # 1 while the handler is running (the re-entrancy guard)
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def active(self) -> bool:
|
|
277
|
+
return self.interval > 0
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def remaining(self) -> int:
|
|
281
|
+
return max(0, self.interval - self.ticks)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
_ALARM_RE = re.compile(
|
|
285
|
+
r"ALARM\s+(\d+)\s+(-?\d+)\s+(-?\d+)\s+(0x[0-9a-fA-F]+)\s+(-?\d+)")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def parse_alarms(text: str) -> dict:
|
|
289
|
+
"""gini_dump's `ALARM <pid> <interval> <ticks> <handler> <on>` lines -> {pid: AlarmState},
|
|
290
|
+
for processes that actually have an alarm set (interval > 0). Absent on a kernel where the
|
|
291
|
+
student hasn't wired sigalarm yet (the fields are all zero -> nothing returned)."""
|
|
292
|
+
out: dict = {}
|
|
293
|
+
for m in _ALARM_RE.finditer(text or ""):
|
|
294
|
+
a = AlarmState(int(m.group(1)), int(m.group(2)), int(m.group(3)),
|
|
295
|
+
m.group(4), int(m.group(5)))
|
|
296
|
+
if a.active:
|
|
297
|
+
out[a.pid] = a
|
|
298
|
+
return out
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def apply_proc_sched(procs, text: str) -> list:
|
|
302
|
+
"""Set the scheduling fields on each Proc from the `PROC …` lines (no-op if the build omits
|
|
303
|
+
them). Returns the same list for chaining."""
|
|
304
|
+
sched = parse_proc_sched(text)
|
|
305
|
+
for p in procs:
|
|
306
|
+
s = sched.get(p.pid)
|
|
307
|
+
if s:
|
|
308
|
+
p.priority, p.tickets = s["priority"], s["tickets"]
|
|
309
|
+
p.level, p.wait_ticks = s["level"], s["wait_ticks"]
|
|
310
|
+
return procs
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def running_pid(procs) -> int | None:
|
|
314
|
+
for p in procs:
|
|
315
|
+
if p.running:
|
|
316
|
+
return p.pid
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# -- shadow manifest -------------------------------------------------------- #
|
|
321
|
+
# The kernel emits one line per SHADOWABLE function so the oracle/AI can tell, deterministically,
|
|
322
|
+
# which student shadows are wired and healthy — the OS analog of "is this router configured".
|
|
323
|
+
# SHADOW <name> present=<0|1> enabled=<0|1> active=<0|1> faults=<n> hash=<hex|baseline>
|
|
324
|
+
# present : a non-stub shadow was compiled in (student wrote something)
|
|
325
|
+
# enabled : the shadow toggle is on
|
|
326
|
+
# active : the dispatcher is currently running the shadow (not the primary)
|
|
327
|
+
# faults : times the shadow crashed and fell back to the primary
|
|
328
|
+
# hash : build-time hash of the student's file ("baseline" = the shipped stub)
|
|
329
|
+
@dataclass
|
|
330
|
+
class ShadowStatus:
|
|
331
|
+
name: str
|
|
332
|
+
present: bool = False
|
|
333
|
+
enabled: bool = False
|
|
334
|
+
active: bool = False
|
|
335
|
+
faults: int = 0
|
|
336
|
+
hash: str = "baseline"
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def is_student(self) -> bool:
|
|
340
|
+
"""A real student submission (not the shipped baseline stub)."""
|
|
341
|
+
return self.present and self.hash not in ("", "baseline")
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def healthy(self) -> bool:
|
|
345
|
+
"""Wired in and running without having crashed back to the primary."""
|
|
346
|
+
return self.active and self.faults == 0
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
_SHADOW_RE = re.compile(r"SHADOW\s+(\S+)\s+(.*)")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def parse_shadow_manifest(text: str) -> dict:
|
|
353
|
+
"""gini_shadowdump lines -> {name: ShadowStatus}. The liveness signal the assignment oracle
|
|
354
|
+
checks first: are the required shadows present, active, and fault-free?"""
|
|
355
|
+
out: dict = {}
|
|
356
|
+
for line in (text or "").splitlines():
|
|
357
|
+
m = _SHADOW_RE.match(line.strip())
|
|
358
|
+
if not m:
|
|
359
|
+
continue
|
|
360
|
+
kv = dict(re.findall(r"(\w+)=(\S+)", m.group(2)))
|
|
361
|
+
out[m.group(1)] = ShadowStatus(
|
|
362
|
+
name=m.group(1),
|
|
363
|
+
present=kv.get("present") == "1",
|
|
364
|
+
enabled=kv.get("enabled") == "1",
|
|
365
|
+
active=kv.get("active") == "1",
|
|
366
|
+
faults=int(kv.get("faults", "0") or 0),
|
|
367
|
+
hash=kv.get("hash", "baseline"))
|
|
368
|
+
return out
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def ready_queue(procs) -> list:
|
|
372
|
+
"""The RUNNABLE processes in scheduling order — the 'who's waiting now, and why' view that
|
|
373
|
+
complements the Gantt (which is who-ran-over-time). Ordered by MLFQ level, then priority
|
|
374
|
+
(lower number = higher), then pid, so the proc the scheduler would tend to favour is first.
|
|
375
|
+
Missing sched fields sort as 0, so it degrades cleanly on an older kernel."""
|
|
376
|
+
ready = [p for p in procs if p.state == "runnable"]
|
|
377
|
+
return sorted(ready, key=lambda p: (p.level or 0, p.priority if p.priority is not None else 0,
|
|
378
|
+
p.pid))
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# current xv6-riscv system-call numbers (fork=1 .. sync=22); custom syscalls (Syscall Builder)
|
|
382
|
+
# start at 23 and are supplied via the `extra` map.
|
|
383
|
+
SYSCALL_NAMES = {
|
|
384
|
+
1: "fork", 2: "exit", 3: "wait", 4: "pipe", 5: "read", 6: "kill", 7: "exec", 8: "fstat",
|
|
385
|
+
9: "chdir", 10: "dup", 11: "getpid", 12: "sbrk", 13: "pause", 14: "uptime", 15: "open",
|
|
386
|
+
16: "write", 17: "mknod", 18: "unlink", 19: "link", 20: "mkdir", 21: "close", 22: "sync",
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def syscall_name(num: int, extra: dict | None = None) -> str:
|
|
391
|
+
if extra and num in extra:
|
|
392
|
+
return extra[num]
|
|
393
|
+
return SYSCALL_NAMES.get(num, f"sys{num}")
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def parse_sccounts(text: str) -> dict:
|
|
397
|
+
"""gini_scdump `SC <num> <count>` lines -> {syscall_number: cumulative_count}."""
|
|
398
|
+
return {int(m.group(1)): int(m.group(2))
|
|
399
|
+
for m in re.finditer(r"SC (\d+) (\d+)", text or "")}
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@dataclass
|
|
403
|
+
class SyscallEvent:
|
|
404
|
+
pid: int
|
|
405
|
+
num: int
|
|
406
|
+
a0: str = "" # first arg (hex) at call time
|
|
407
|
+
ret: str = "" # return value (hex)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def parse_sctrace(text: str) -> list:
|
|
411
|
+
"""gini_scdump `TRACE <pid> <num> <a0> <ret>` lines -> [SyscallEvent] (oldest -> newest)."""
|
|
412
|
+
out: list = []
|
|
413
|
+
for m in re.finditer(r"TRACE (\d+) (\d+) (0x[0-9a-fA-F]+) (0x[0-9a-fA-F]+)", text or ""):
|
|
414
|
+
out.append(SyscallEvent(int(m.group(1)), int(m.group(2)), m.group(3), m.group(4)))
|
|
415
|
+
return out
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class SyscallRate:
|
|
419
|
+
"""Rolling per-syscall call count over a time window (default 60s) — the histogram feed.
|
|
420
|
+
Feed cumulative `{num: count}` snapshots with their wall-clock time; `rates()` returns how
|
|
421
|
+
many of each syscall happened in the last `window` seconds (now-count minus the count as of
|
|
422
|
+
~window ago). Before the window fills, it uses the oldest snapshot (calls-so-far)."""
|
|
423
|
+
|
|
424
|
+
def __init__(self, window: float = 60.0, cap: int = 300) -> None:
|
|
425
|
+
from collections import deque
|
|
426
|
+
self.window = window
|
|
427
|
+
self.snaps = deque(maxlen=cap) # (t, {num: cumulative_count})
|
|
428
|
+
|
|
429
|
+
def add(self, t: float, counts: dict) -> None:
|
|
430
|
+
self.snaps.append((t, dict(counts)))
|
|
431
|
+
|
|
432
|
+
def rates(self) -> dict:
|
|
433
|
+
if not self.snaps:
|
|
434
|
+
return {}
|
|
435
|
+
now_t, now = self.snaps[-1]
|
|
436
|
+
cutoff = now_t - self.window
|
|
437
|
+
base = self.snaps[0][1] # oldest, until we have a snapshot past the cutoff
|
|
438
|
+
for t, c in self.snaps:
|
|
439
|
+
if t <= cutoff:
|
|
440
|
+
base = c
|
|
441
|
+
else:
|
|
442
|
+
break
|
|
443
|
+
# On the very first snapshot there's no earlier baseline (oldest == now), so a delta would
|
|
444
|
+
# be 0 across the board. Treat pre-observation as empty -> the histogram shows counts-so-far
|
|
445
|
+
# immediately (the "calls-so-far" the docstring promises) instead of a blank first read.
|
|
446
|
+
if len(self.snaps) == 1:
|
|
447
|
+
base = {}
|
|
448
|
+
out = {num: now[num] - base.get(num, 0) for num in now}
|
|
449
|
+
return {k: v for k, v in out.items() if v > 0}
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
# -- traps & interrupts (the trap-taxonomy ring; gini_trapdump over Ctrl-R) ------------------- #
|
|
453
|
+
TRAP_KINDS = {0: "syscall", 1: "pagefault", 2: "timer", 3: "device", 4: "illegal", 5: "other"}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def trap_kind_name(kind: int) -> str:
|
|
457
|
+
return TRAP_KINDS.get(kind, f"kind{kind}")
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def parse_trapcounts(text: str) -> dict:
|
|
461
|
+
"""gini_trapdump `TC <kind> <name> <count>` lines -> {kind_index: cumulative_count}."""
|
|
462
|
+
return {int(m.group(1)): int(m.group(2))
|
|
463
|
+
for m in re.finditer(r"TC (\d+) \w+ (\d+)", text or "")}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
@dataclass
|
|
467
|
+
class TrapEvent:
|
|
468
|
+
pid: int
|
|
469
|
+
kind: int
|
|
470
|
+
cause: str = "" # scause (hex); interrupt causes have the top bit set
|
|
471
|
+
epc: str = "" # faulting / trapping PC (hex)
|
|
472
|
+
tval: str = "" # stval — faulting address for page faults (hex)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def parse_traptrace(text: str) -> list:
|
|
476
|
+
"""gini_trapdump `TR <pid> <kind> <cause> <epc> <tval>` lines -> [TrapEvent] (oldest->newest)."""
|
|
477
|
+
out: list = []
|
|
478
|
+
for m in re.finditer(
|
|
479
|
+
r"TR (\d+) (\d+) (0x[0-9a-fA-F]+) (0x[0-9a-fA-F]+) (0x[0-9a-fA-F]+)", text or ""):
|
|
480
|
+
out.append(TrapEvent(int(m.group(1)), int(m.group(2)),
|
|
481
|
+
m.group(3), m.group(4), m.group(5)))
|
|
482
|
+
return out
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
class TrapRate(SyscallRate):
|
|
486
|
+
"""Rolling per-trap-kind counts over a window (default 60s) — the trap histogram feed.
|
|
487
|
+
Same mechanics as SyscallRate (cumulative snapshots -> deltas in the window), keyed by trap
|
|
488
|
+
kind index instead of syscall number, so `rates()` returns traps-per-kind in the last window."""
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
# -- freeze a real trap (Phase 2: /trapcatch -> seed the CPU journey with live values) --------- #
|
|
492
|
+
# RISC-V S-mode exception codes -> a human name (interrupts are handled separately, by code).
|
|
493
|
+
_SCAUSE_EXC = {
|
|
494
|
+
0: "instruction address misaligned", 1: "instruction access fault", 2: "illegal instruction",
|
|
495
|
+
3: "breakpoint", 4: "load address misaligned", 5: "load access fault",
|
|
496
|
+
6: "store address misaligned", 7: "store access fault", 8: "ecall from U-mode (syscall)",
|
|
497
|
+
12: "instruction page fault", 13: "load page fault", 15: "store page fault",
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def decode_scause(cause) -> tuple:
|
|
502
|
+
"""An scause value (hex string or int) -> (kind_index, human_name). Mirrors the kernel
|
|
503
|
+
gini_kind() bucketing, but adds the specific exception/interrupt name for the journey caption."""
|
|
504
|
+
try:
|
|
505
|
+
c = int(cause, 16) if isinstance(cause, str) else int(cause)
|
|
506
|
+
except (ValueError, TypeError):
|
|
507
|
+
return 5, "other"
|
|
508
|
+
if c & (1 << 63): # interrupt (top bit set)
|
|
509
|
+
code = c & 0xff
|
|
510
|
+
if code == 9:
|
|
511
|
+
return 3, "supervisor external interrupt (device)"
|
|
512
|
+
if code == 5:
|
|
513
|
+
return 2, "supervisor timer interrupt"
|
|
514
|
+
if code == 1:
|
|
515
|
+
return 2, "supervisor software interrupt"
|
|
516
|
+
return 2, f"interrupt (code {code})"
|
|
517
|
+
code = c & 0xff
|
|
518
|
+
if code == 8:
|
|
519
|
+
return 0, _SCAUSE_EXC[8]
|
|
520
|
+
if code in (12, 13, 15):
|
|
521
|
+
return 1, _SCAUSE_EXC[code]
|
|
522
|
+
if code == 2:
|
|
523
|
+
return 4, _SCAUSE_EXC[2]
|
|
524
|
+
return 5, _SCAUSE_EXC.get(code, f"exception (code {code})")
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
@dataclass
|
|
528
|
+
class TrapFrame:
|
|
529
|
+
"""A single trap frozen at usertrap entry (from /trapcatch): the trap CSRs plus the user
|
|
530
|
+
registers uservec saved into the trapframe. `ok` is False when the catch timed out (idle
|
|
531
|
+
kernel) — the journey then falls back to its authored captions."""
|
|
532
|
+
scause: str = ""
|
|
533
|
+
sepc: str = ""
|
|
534
|
+
stval: str = ""
|
|
535
|
+
pid: int | None = None
|
|
536
|
+
regs: dict = field(default_factory=dict) # epc/ra/sp/a0../a7 (hex strings)
|
|
537
|
+
kind: int = 5
|
|
538
|
+
kind_name: str = "other"
|
|
539
|
+
ok: bool = False
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def parse_trapframe(text: str) -> TrapFrame:
|
|
543
|
+
"""Parse the agent's /trapcatch gdb output (`key 0x…` lines after a ===TRAP=== marker) into a
|
|
544
|
+
TrapFrame. Missing/garbled fields are tolerated; ok=True only once we have a valid scause."""
|
|
545
|
+
fr = TrapFrame()
|
|
546
|
+
body = (text or "").split("===TRAP===", 1)[-1]
|
|
547
|
+
if "gdb-timeout" in (text or "") or "gdb-error" in (text or ""):
|
|
548
|
+
return fr # ok stays False -> authored fallback
|
|
549
|
+
for line in body.splitlines():
|
|
550
|
+
m = re.match(r"\s*([a-z]\w*)\s+(0x[0-9a-fA-F]+|-?\d+)\s*$", line)
|
|
551
|
+
if not m:
|
|
552
|
+
continue
|
|
553
|
+
key, val = m.group(1), m.group(2)
|
|
554
|
+
if key in ("scause", "sepc", "stval"):
|
|
555
|
+
setattr(fr, key, val)
|
|
556
|
+
elif key == "pid":
|
|
557
|
+
try:
|
|
558
|
+
fr.pid = int(val)
|
|
559
|
+
except ValueError:
|
|
560
|
+
pass
|
|
561
|
+
elif key in ("epc", "ra", "sp", "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7"):
|
|
562
|
+
fr.regs[key] = val
|
|
563
|
+
if fr.scause:
|
|
564
|
+
fr.kind, fr.kind_name = decode_scause(fr.scause)
|
|
565
|
+
fr.ok = True
|
|
566
|
+
return fr
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
@dataclass
|
|
570
|
+
class TreeNode:
|
|
571
|
+
proc: Proc
|
|
572
|
+
depth: int
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def build_process_tree(procs) -> list:
|
|
576
|
+
"""Order procs as a depth-first process TREE by parent links, returning [TreeNode(proc, depth)]
|
|
577
|
+
in display order (depth = indentation). Roots are procs whose parent is 0/None/missing (init).
|
|
578
|
+
Defensive against cycles and orphans so a bad read can never hang or drop a process."""
|
|
579
|
+
by_pid = {p.pid: p for p in procs}
|
|
580
|
+
children: dict = {}
|
|
581
|
+
roots: list = []
|
|
582
|
+
for p in procs:
|
|
583
|
+
par = p.parent
|
|
584
|
+
if par and par in by_pid and par != p.pid:
|
|
585
|
+
children.setdefault(par, []).append(p)
|
|
586
|
+
else:
|
|
587
|
+
roots.append(p)
|
|
588
|
+
out: list = []
|
|
589
|
+
seen: set = set()
|
|
590
|
+
|
|
591
|
+
def walk(p, depth):
|
|
592
|
+
if p.pid in seen: # cycle guard
|
|
593
|
+
return
|
|
594
|
+
seen.add(p.pid)
|
|
595
|
+
out.append(TreeNode(p, depth))
|
|
596
|
+
for c in sorted(children.get(p.pid, []), key=lambda c: c.pid):
|
|
597
|
+
walk(c, depth + 1)
|
|
598
|
+
|
|
599
|
+
for r in sorted(roots, key=lambda p: p.pid):
|
|
600
|
+
walk(r, 0)
|
|
601
|
+
for p in procs: # any left over (cycle) -> show at root
|
|
602
|
+
if p.pid not in seen:
|
|
603
|
+
out.append(TreeNode(p, 0))
|
|
604
|
+
return out
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
# -- scheduling timeline (Gantt) -------------------------------------------- #
|
|
608
|
+
@dataclass
|
|
609
|
+
class Slot:
|
|
610
|
+
tick: int | None
|
|
611
|
+
pid: int | None
|
|
612
|
+
name: str
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
class SchedTimeline:
|
|
616
|
+
"""Accumulate which process is RUNNING across snapshots — the Gantt strip that makes
|
|
617
|
+
context switches visible. Records a slot only when the running process changes (a switch)
|
|
618
|
+
or the tick advances, so the strip reads as the switch history."""
|
|
619
|
+
|
|
620
|
+
def __init__(self, cap: int = 400) -> None:
|
|
621
|
+
self.slots: list[Slot] = []
|
|
622
|
+
self.cap = cap
|
|
623
|
+
self._last_pid: int | None = None
|
|
624
|
+
|
|
625
|
+
def add(self, snap: Snapshot) -> None:
|
|
626
|
+
pid = snap.running_pid if snap.running_pid is not None else running_pid(snap.procs)
|
|
627
|
+
name = next((p.name for p in snap.procs if p.pid == pid), "")
|
|
628
|
+
self.add_run(pid, snap.ticks, name)
|
|
629
|
+
|
|
630
|
+
def add_run(self, pid, ticks, name="") -> None:
|
|
631
|
+
"""Record a single (pid, ticks) sample — used per-CPU on SMP as well as the aggregate."""
|
|
632
|
+
if pid != self._last_pid or (self.slots and self.slots[-1].tick != ticks):
|
|
633
|
+
self.slots.append(Slot(ticks, pid, name))
|
|
634
|
+
self._last_pid = pid
|
|
635
|
+
if len(self.slots) > self.cap:
|
|
636
|
+
self.slots = self.slots[-self.cap:]
|
|
637
|
+
|
|
638
|
+
def recent(self, n: int = 60) -> list[Slot]:
|
|
639
|
+
return self.slots[-n:]
|
|
640
|
+
|
|
641
|
+
def switches(self) -> int:
|
|
642
|
+
return max(0, sum(1 for i in range(1, len(self.slots))
|
|
643
|
+
if self.slots[i].pid != self.slots[i - 1].pid))
|
|
644
|
+
|
|
645
|
+
def shares(self, n: int = 60) -> dict:
|
|
646
|
+
"""CPU share over the last `n` slots: {pid: fraction of slots it held} (idle excluded).
|
|
647
|
+
This is the evidence a lottery/fairness assignment leans on — the share should track the
|
|
648
|
+
ticket ratio. Coarse (the timeline is sampled), but a real observation of the live kernel."""
|
|
649
|
+
pids = [s.pid for s in self.slots[-n:] if s.pid is not None]
|
|
650
|
+
total = len(pids)
|
|
651
|
+
if not total:
|
|
652
|
+
return {}
|
|
653
|
+
from collections import Counter
|
|
654
|
+
return {pid: c / total for pid, c in Counter(pids).items()}
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
# -- offline demo provider -------------------------------------------------- #
|
|
658
|
+
# A pure, deterministic feed over a few procs, so the Machine Lab is explorable (and testable)
|
|
659
|
+
# without a live QEMU/GDB. It honours the same policies as the real gini_pick() so the scheduler
|
|
660
|
+
# face behaves offline: round-robin, priority (with aging), and lottery (ticket-weighted). On the
|
|
661
|
+
# Mac a real GDB bridge replaces this with the same Snapshot shape.
|
|
662
|
+
_DEMO_PROCS = [(1, "init"), (2, "sh"), (3, "spin"), (4, "spin"), (5, "primes")]
|
|
663
|
+
# per-proc scheduling params for the CPU-bound demo procs (init/sh sleep, so they don't compete).
|
|
664
|
+
_DEMO_META = {3: {"priority": 5, "tickets": 1}, # high priority, few tickets
|
|
665
|
+
4: {"priority": 10, "tickets": 2},
|
|
666
|
+
5: {"priority": 10, "tickets": 4}} # low priority, most tickets
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
class DemoScheduler:
|
|
670
|
+
"""Deterministic policy-aware feed of Snapshots — the offline stand-in for the GDB bridge.
|
|
671
|
+
Mirrors gini_pick(): round-robin / priority (aging) / lottery, so switching the policy in the
|
|
672
|
+
UI visibly changes who runs even with no container attached."""
|
|
673
|
+
|
|
674
|
+
def __init__(self, timeslice: int = 1, policy: str = "round-robin") -> None:
|
|
675
|
+
self.timeslice = max(1, int(timeslice))
|
|
676
|
+
self.policy = policy
|
|
677
|
+
self._ticks = 0
|
|
678
|
+
self._runnable = [3, 4, 5]
|
|
679
|
+
self._run = 3 # currently running pid (starts on the first CPU-bound proc)
|
|
680
|
+
self._rr_ix = 2 # round-robin cursor into _DEMO_PROCS
|
|
681
|
+
self._wait = {p: 0 for p in self._runnable} # aging counters (priority policy)
|
|
682
|
+
self._seed = 2463534242 # xorshift PRNG state (lottery), fixed -> deterministic
|
|
683
|
+
|
|
684
|
+
def set_timeslice(self, ticks: int) -> None:
|
|
685
|
+
self.timeslice = max(1, int(ticks))
|
|
686
|
+
|
|
687
|
+
def set_policy(self, policy) -> None:
|
|
688
|
+
"""Accept a policy name ('priority') or its numeric id (1)."""
|
|
689
|
+
self.policy = POLICY_NAMES.get(policy, policy) if isinstance(policy, int) else policy
|
|
690
|
+
|
|
691
|
+
def sc(self) -> str:
|
|
692
|
+
"""Offline demo of gini_scdump — growing syscall counts + a few recent calls, so the
|
|
693
|
+
histogram/trace panels are explorable without a container."""
|
|
694
|
+
self._sc_t = getattr(self, "_sc_t", 0) + 1
|
|
695
|
+
n = self._sc_t
|
|
696
|
+
lines = [f"SC 1 {n}", f"SC 16 {n * 7}", f"SC 5 {n * 3}", f"SC 13 {n * 2}", f"SC 12 {n}"]
|
|
697
|
+
trace = ["TRACE 2 1 0x0 0x5", "TRACE 5 16 0x4 0x6", "TRACE 5 12 0x1000 0x4000",
|
|
698
|
+
"TRACE 2 3 0x0 0x5"]
|
|
699
|
+
return "\n".join(lines + trace) + "\n"
|
|
700
|
+
|
|
701
|
+
def traps(self) -> str:
|
|
702
|
+
"""Offline demo of gini_trapdump — a plausible growing trap mix so the Traps face is
|
|
703
|
+
explorable without a container: mostly timer, a syscall trickle, an occasional page
|
|
704
|
+
fault. Counters are cumulative (TrapRate turns them into a 60s window)."""
|
|
705
|
+
self._tr_t = getattr(self, "_tr_t", 0) + 1
|
|
706
|
+
n = self._tr_t
|
|
707
|
+
kinds = [(0, "syscall", n * 4), (1, "pagefault", n // 2), (2, "timer", n * 9),
|
|
708
|
+
(3, "device", n // 3), (4, "illegal", 0), (5, "other", 0)]
|
|
709
|
+
tc = [f"TC {k} {name} {cnt}" for k, name, cnt in kinds]
|
|
710
|
+
tr = ["TR 5 2 0x8000000000000005 0x0000000000001050 0x0", # a timer interrupt
|
|
711
|
+
"TR 5 1 0x000000000000000f 0x0000000000001080 0x0000000000004000", # store fault
|
|
712
|
+
"TR 2 0 0x0000000000000008 0x0000000000001d3c 0x0", # a syscall (ecall)
|
|
713
|
+
"TR 5 2 0x8000000000000005 0x0000000000001054 0x0"]
|
|
714
|
+
return "\n".join(tc + tr) + "\n"
|
|
715
|
+
|
|
716
|
+
def catch_trap(self, kind: str = "any") -> "TrapFrame":
|
|
717
|
+
"""Offline demo of /trapcatch — a plausible frozen trap, so the CPU journey can be seeded
|
|
718
|
+
with real-looking values without a container. Honours the requested `kind` where it can."""
|
|
719
|
+
if kind == "syscall":
|
|
720
|
+
return TrapFrame(scause="0x0000000000000008", sepc="0x0000000000001d3c", stval="0x0",
|
|
721
|
+
pid=2, regs={"epc": "0x1d3c", "a0": "0x0", "a7": "0x7"},
|
|
722
|
+
kind=0, kind_name="ecall from U-mode (syscall)", ok=True)
|
|
723
|
+
if kind == "timer":
|
|
724
|
+
return TrapFrame(scause="0x8000000000000005", sepc="0x0000000000001050", stval="0x0",
|
|
725
|
+
pid=5, regs={"epc": "0x1050", "sp": "0x3fffff9000"},
|
|
726
|
+
kind=2, kind_name="supervisor timer interrupt", ok=True)
|
|
727
|
+
return TrapFrame( # default / "pagefault": a store fault
|
|
728
|
+
scause="0x000000000000000f", sepc="0x0000000000001080",
|
|
729
|
+
stval="0x0000000000004000", pid=5,
|
|
730
|
+
regs={"epc": "0x0000000000001080", "ra": "0x0000000000001d3c",
|
|
731
|
+
"sp": "0x0000003fffff9000", "a0": "0x0000000000000005", "a7": "0x000000000000000f"},
|
|
732
|
+
kind=1, kind_name="store page fault", ok=True)
|
|
733
|
+
|
|
734
|
+
def alarms(self) -> str:
|
|
735
|
+
"""Offline demo of the ALARM dump lines — one process with a periodic alarm whose
|
|
736
|
+
countdown advances each call, so the alarm strip animates without a container."""
|
|
737
|
+
self._al_t = getattr(self, "_al_t", 0) + 1
|
|
738
|
+
interval = 10
|
|
739
|
+
ticks = self._al_t % (interval + 1)
|
|
740
|
+
on = 1 if ticks == 0 else 0
|
|
741
|
+
return f"ALARM 5 {interval} {ticks} 0x0000000000001120 {on}\n"
|
|
742
|
+
|
|
743
|
+
def _pick(self) -> int:
|
|
744
|
+
"""Choose the next running pid per policy — the offline mirror of kernel gini_pick()."""
|
|
745
|
+
rn = self._runnable
|
|
746
|
+
if self.policy == "priority":
|
|
747
|
+
best, best_eff = None, None
|
|
748
|
+
for p in rn:
|
|
749
|
+
self._wait[p] += 1 # aging: waiting raises effective priority
|
|
750
|
+
eff = _DEMO_META[p]["priority"] - self._wait[p] // 4
|
|
751
|
+
if best is None or eff < best_eff:
|
|
752
|
+
best, best_eff = p, eff
|
|
753
|
+
self._wait[best] = 0
|
|
754
|
+
return best
|
|
755
|
+
if self.policy == "lottery":
|
|
756
|
+
total = sum(_DEMO_META[p]["tickets"] for p in rn)
|
|
757
|
+
self._seed ^= (self._seed << 13) & 0xFFFFFFFF
|
|
758
|
+
self._seed ^= self._seed >> 17
|
|
759
|
+
self._seed ^= (self._seed << 5) & 0xFFFFFFFF
|
|
760
|
+
win, acc = self._seed % total, 0
|
|
761
|
+
for p in rn:
|
|
762
|
+
acc += _DEMO_META[p]["tickets"]
|
|
763
|
+
if win < acc:
|
|
764
|
+
return p
|
|
765
|
+
return rn[-1]
|
|
766
|
+
# round-robin
|
|
767
|
+
self._rr_ix = (self._rr_ix + 1) % len(_DEMO_PROCS)
|
|
768
|
+
while _DEMO_PROCS[self._rr_ix][0] not in rn:
|
|
769
|
+
self._rr_ix = (self._rr_ix + 1) % len(_DEMO_PROCS)
|
|
770
|
+
return _DEMO_PROCS[self._rr_ix][0]
|
|
771
|
+
|
|
772
|
+
def step(self) -> "Snapshot":
|
|
773
|
+
"""Advance one context switch and return the new snapshot."""
|
|
774
|
+
self._ticks += self.timeslice
|
|
775
|
+
self._run = self._pick()
|
|
776
|
+
return self.snapshot()
|
|
777
|
+
|
|
778
|
+
def snapshot(self) -> "Snapshot":
|
|
779
|
+
run_pid = self._run
|
|
780
|
+
procs = []
|
|
781
|
+
for pid, name in _DEMO_PROCS:
|
|
782
|
+
st = ("running" if pid == run_pid
|
|
783
|
+
else "runnable" if pid in self._runnable else "sleeping")
|
|
784
|
+
meta = _DEMO_META.get(pid)
|
|
785
|
+
procs.append(Proc(pid, st, name,
|
|
786
|
+
priority=meta["priority"] if meta else None,
|
|
787
|
+
tickets=meta["tickets"] if meta else None,
|
|
788
|
+
level=0 if meta else None,
|
|
789
|
+
wait_ticks=self._wait.get(pid)))
|
|
790
|
+
pc = 0x80001000 + (run_pid * 0x40) + (self._ticks & 0xF)
|
|
791
|
+
cpu = CpuState(regs={
|
|
792
|
+
"pc": hex(pc), "sp": hex(0x3FFFFF9000 - run_pid * 0x1000),
|
|
793
|
+
"ra": hex(0x80001D3C), "s0": hex(0x3FFFFF9F00 - run_pid * 0x1000),
|
|
794
|
+
"satp": hex(0x8000000000080000 + run_pid),
|
|
795
|
+
"a0": hex(run_pid), "a7": "0x7"})
|
|
796
|
+
stack = [Frame("swtch", "kernel/swtch.S:20"),
|
|
797
|
+
Frame("sched", "kernel/proc.c:493"),
|
|
798
|
+
Frame("yield", "kernel/proc.c:515"),
|
|
799
|
+
Frame("usertrap", "kernel/trap.c:67")]
|
|
800
|
+
# a plausible, advancing mode-time (≈70% user / 20% kernel / 10% idle) so the CPU face's
|
|
801
|
+
# bar animates offline; + representative CSRs: SPP=U (came from a user proc), all three
|
|
802
|
+
# S-interrupts enabled, a timer scause. Grounded in real bit layouts, not the wire.
|
|
803
|
+
modetime = {"user": self._ticks * 7, "kernel": self._ticks * 2, "idle": self._ticks}
|
|
804
|
+
csr = {"sstatus": SSTATUS_SPIE, "sie": (1 << 1) | (1 << 5) | (1 << 9),
|
|
805
|
+
"sip": 1 << 5, "stvec": 0x80001BB4,
|
|
806
|
+
"scause": 0x8000000000000005, "sepc": pc}
|
|
807
|
+
return Snapshot(procs=procs, running_pid=run_pid, ticks=self._ticks,
|
|
808
|
+
cpu=cpu, stack=stack, modetime=modetime, csr=csr, source="demo")
|