Skip to content

Commit 27442a0

Browse files
committed
TUI improvements
Signed-off-by: Matthew Ballance <matt.ballance@gmail.com>
1 parent 3d68bfa commit 27442a0

6 files changed

Lines changed: 555 additions & 240 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ See documentation for complete import examples and supported formats.
185185
## Documentation
186186

187187
- [MCP Server Documentation](MCP_SERVER.md)
188+
- [Python SQLite Performance Report](PYTHON_SQLITE_PERFORMANCE_REPORT.md)
188189
- [UCIS Standard](https://www.accellera.org/activities/committees/ucis)
189190

190191
## License
@@ -196,4 +197,3 @@ Apache 2.0
196197
- [PyPI](https://pypi.org/project/pyucis/)
197198
- [GitHub](https://github.com/fvutils/pyucis)
198199
- [Issue Tracker](https://github.com/fvutils/pyucis/issues)
199-

src/ucis/sqlite/schema_manager.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ def create_schema(conn: sqlite3.Connection):
8888
)
8989
""")
9090

91-
cursor.execute("CREATE INDEX IF NOT EXISTS idx_scopes_parent ON scopes(parent_id)")
9291
cursor.execute("CREATE INDEX IF NOT EXISTS idx_scopes_parent_type_name ON scopes(parent_id, scope_type, scope_name)")
9392

9493
# 4. Cover Items
@@ -116,7 +115,6 @@ def create_schema(conn: sqlite3.Connection):
116115
)
117116
""")
118117

119-
cursor.execute("CREATE INDEX IF NOT EXISTS idx_coveritems_scope_index ON coveritems(scope_id, cover_index)")
120118

121119
# 5. History Nodes
122120
cursor.execute("""
@@ -162,7 +160,6 @@ def create_schema(conn: sqlite3.Connection):
162160
)
163161
""")
164162

165-
cursor.execute("CREATE INDEX IF NOT EXISTS idx_coveritem_tests_cover ON coveritem_tests(cover_id)")
166163
cursor.execute("CREATE INDEX IF NOT EXISTS idx_coveritem_tests_history ON coveritem_tests(history_id)")
167164

168165
# 7. Properties Tables
@@ -512,3 +509,10 @@ def ensure_schema_current(conn: sqlite3.Connection):
512509
f"Database schema version mismatch: found {version}, expected {SCHEMA_VERSION}. "
513510
f"Please recreate the database with the current schema version."
514511
)
512+
513+
# Remove known redundant indexes to reduce DB size and write overhead
514+
cursor = conn.cursor()
515+
cursor.execute("DROP INDEX IF EXISTS idx_coveritems_scope_index")
516+
cursor.execute("DROP INDEX IF EXISTS idx_scopes_parent")
517+
cursor.execute("DROP INDEX IF EXISTS idx_coveritem_tests_cover")
518+
conn.commit()

src/ucis/tui/controller.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ def __init__(self, coverage_model, on_quit: Optional[Callable] = None):
3939
# Status messages
4040
self.status_message = ""
4141
self.status_type = "info" # "info", "error", "success"
42+
self._view_order = [
43+
"dashboard",
44+
"hierarchy",
45+
"gaps",
46+
"hotspots",
47+
"metrics",
48+
"code_coverage",
49+
"test_history",
50+
]
4251

4352
def register_view(self, name: str, view):
4453
"""Register a view with the controller."""
@@ -115,6 +124,11 @@ def handle_key(self, key: str) -> bool:
115124
if self.show_help:
116125
self.show_help = False
117126
return True
127+
128+
# Global page navigation with arrows
129+
if key in ("left", "right") and self.current_view_name:
130+
if self._switch_adjacent_view(-1 if key == "left" else 1):
131+
return True
118132

119133
# Give view first chance to handle key
120134
current_view = self.get_current_view()
@@ -124,6 +138,19 @@ def handle_key(self, key: str) -> bool:
124138

125139
# If view didn't handle it, try global keys
126140
return self._handle_global_key(key)
141+
142+
def _switch_adjacent_view(self, direction: int) -> bool:
143+
"""Switch to previous/next registered top-level view."""
144+
if self.current_view_name not in self._view_order:
145+
return False
146+
147+
available = [v for v in self._view_order if v in self.views]
148+
if len(available) < 2:
149+
return False
150+
151+
idx = available.index(self.current_view_name)
152+
new_idx = (idx + direction) % len(available)
153+
return self.switch_view(available[new_idx])
127154

128155
def _handle_global_key(self, key: str) -> bool:
129156
"""

src/ucis/tui/models/coverage_model.py

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,37 @@ def get_summary(self) -> Dict[str, Any]:
6767
'by_type': {}
6868
}
6969

70-
# Walk through database to compute statistics
70+
# Fast path for SQLite backends
71+
if self.db and hasattr(self.db, 'conn'):
72+
from ucis.cover_type_t import CoverTypeT
73+
from ucis.scope_type_t import ScopeTypeT
74+
try:
75+
conn = self.db.conn
76+
summary['covergroups'] = conn.execute(
77+
"SELECT COUNT(*) FROM scopes WHERE (scope_type & ?) != 0",
78+
(int(ScopeTypeT.COVERGROUP),)
79+
).fetchone()[0]
80+
summary['coverpoints'] = conn.execute(
81+
"SELECT COUNT(*) FROM scopes WHERE (scope_type & ?) != 0",
82+
(int(ScopeTypeT.COVERPOINT),)
83+
).fetchone()[0]
84+
row = conn.execute(
85+
"""SELECT COUNT(*),
86+
SUM(CASE WHEN cover_data > 0 THEN 1 ELSE 0 END)
87+
FROM coveritems
88+
WHERE (cover_type & ?) != 0""",
89+
(int(CoverTypeT.CVGBIN),)
90+
).fetchone()
91+
summary['total_bins'] = row[0] or 0
92+
summary['covered_bins'] = row[1] or 0
93+
if summary['total_bins'] > 0:
94+
summary['overall_coverage'] = (summary['covered_bins'] / summary['total_bins']) * 100
95+
self._cache['summary'] = summary
96+
return summary
97+
except Exception:
98+
pass
99+
100+
# Walk through database to compute statistics (fallback)
71101
def visit_scope(scope, depth=0):
72102
from ucis.scope_type_t import ScopeTypeT
73103
from ucis.cover_type_t import CoverTypeT
@@ -125,8 +155,15 @@ def get_database_info(self) -> Dict[str, Any]:
125155
# Get test data if available
126156
if self.db:
127157
try:
128-
tests = self.get_all_tests()
129-
info['test_count'] = len(tests)
158+
if hasattr(self.db, 'conn'):
159+
from ucis.history_node_kind import HistoryNodeKind
160+
info['test_count'] = self.db.conn.execute(
161+
"SELECT COUNT(*) FROM history_nodes WHERE history_kind = ?",
162+
(int(HistoryNodeKind.TEST),)
163+
).fetchone()[0]
164+
else:
165+
tests = self.get_all_tests()
166+
info['test_count'] = len(tests)
130167
except:
131168
pass
132169

@@ -147,6 +184,25 @@ def get_coverage_types(self) -> List[CoverTypeT]:
147184
"""
148185
if 'coverage_types' in self._cache:
149186
return self._cache['coverage_types']
187+
188+
# Fast path for SQLite backend
189+
if self.db and hasattr(self.db, 'conn'):
190+
try:
191+
rows = self.db.conn.execute(
192+
"SELECT DISTINCT cover_type FROM coveritems ORDER BY cover_type"
193+
).fetchall()
194+
types_list = []
195+
for r in rows:
196+
if r[0] is None:
197+
continue
198+
try:
199+
types_list.append(CoverTypeT(r[0]))
200+
except Exception:
201+
pass
202+
self._cache['coverage_types'] = types_list
203+
return types_list
204+
except Exception:
205+
pass
150206

151207
types_found: Set[CoverTypeT] = set()
152208

@@ -206,6 +262,30 @@ def get_code_coverage_summary(self) -> Dict[str, Any]:
206262
CoverTypeT.FSMBIN: 'fsm',
207263
CoverTypeT.BLOCKBIN: 'block',
208264
}
265+
266+
# Fast path for SQLite backend
267+
if self.db and hasattr(self.db, 'conn'):
268+
try:
269+
rows = self.db.conn.execute(
270+
"""SELECT cover_type,
271+
COUNT(*) AS total,
272+
SUM(CASE WHEN cover_data > 0 THEN 1 ELSE 0 END) AS covered
273+
FROM coveritems
274+
GROUP BY cover_type"""
275+
).fetchall()
276+
int_type_map = {int(k): v for k, v in type_map.items()}
277+
for row in rows:
278+
key = int_type_map.get(row[0])
279+
if key is not None:
280+
summary[key]['total'] = row[1] or 0
281+
summary[key]['covered'] = row[2] or 0
282+
for key in summary:
283+
if summary[key]['total'] > 0:
284+
summary[key]['coverage'] = (summary[key]['covered'] / summary[key]['total']) * 100
285+
self._cache['code_coverage_summary'] = summary
286+
return summary
287+
except Exception:
288+
pass
209289

210290
def visit_scope(scope):
211291
for cov_type, key in type_map.items():
@@ -268,6 +348,37 @@ def get_coverage_by_type(self, cov_type: CoverTypeT, filtered: bool = True) -> D
268348
'covered': 0,
269349
'coverage': 0.0
270350
}
351+
352+
# Fast path for SQLite backend
353+
if self.db and hasattr(self.db, 'conn'):
354+
try:
355+
if filter_active:
356+
row = self.db.conn.execute(
357+
"""SELECT COUNT(*),
358+
SUM(CASE WHEN ci.cover_data > 0 THEN 1 ELSE 0 END)
359+
FROM coveritems ci
360+
JOIN coveritem_tests ct ON ct.cover_id = ci.cover_id
361+
JOIN history_nodes hn ON hn.history_id = ct.history_id
362+
WHERE (ci.cover_type & ?) != 0
363+
AND hn.logical_name = ?""",
364+
(int(cov_type), self.test_filter)
365+
).fetchone()
366+
else:
367+
row = self.db.conn.execute(
368+
"""SELECT COUNT(*),
369+
SUM(CASE WHEN cover_data > 0 THEN 1 ELSE 0 END)
370+
FROM coveritems
371+
WHERE (cover_type & ?) != 0""",
372+
(int(cov_type),)
373+
).fetchone()
374+
result['total'] = row[0] or 0
375+
result['covered'] = row[1] or 0
376+
if result['total'] > 0:
377+
result['coverage'] = (result['covered'] / result['total']) * 100
378+
self._cache[cache_key] = result
379+
return result
380+
except Exception:
381+
pass
271382

272383
def visit_scope(scope):
273384
try:

0 commit comments

Comments
 (0)