Skip to content

Commit 711443a

Browse files
committed
Feat(viewer): Add bloat analysis
1 parent 38aa6cc commit 711443a

3 files changed

Lines changed: 174 additions & 0 deletions

File tree

codeclash/viewer/app.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,72 @@ def get_available_trajectories(self) -> list[tuple]:
527527

528528
return sorted(trajectories)
529529

530+
def analyze_line_counts(self) -> dict[str, Any]:
531+
"""Analyze line counts across all rounds for all files that appear in changed files"""
532+
# Collect all files that appear in any changed files across all rounds and players
533+
all_files = set()
534+
players_dir = self.log_dir / "players"
535+
536+
if not players_dir.exists():
537+
return {"all_files": [], "line_counts_by_round": {}}
538+
539+
# First pass: collect all files from all changes_r*.json files
540+
for player_dir in players_dir.iterdir():
541+
if not player_dir.is_dir():
542+
continue
543+
544+
for changes_file in player_dir.glob("changes_r*.json"):
545+
try:
546+
changes_data = json.loads(changes_file.read_text())
547+
modified_files = changes_data.get("modified_files", {})
548+
all_files.update(modified_files.keys())
549+
except (json.JSONDecodeError, KeyError):
550+
continue
551+
552+
all_files_list = sorted(list(all_files))
553+
554+
# Second pass: count lines for each file in each round for each player
555+
line_counts_by_round = {}
556+
557+
for player_dir in players_dir.iterdir():
558+
if not player_dir.is_dir():
559+
continue
560+
561+
player_name = player_dir.name
562+
563+
# Get all rounds for this player
564+
changes_files = sorted(player_dir.glob("changes_r*.json"), key=lambda x: int(x.stem.split("_r")[1]))
565+
566+
# Track line counts for this player across rounds
567+
player_line_counts = {}
568+
current_file_lines = {} # Track current state of each file
569+
570+
for changes_file in changes_files:
571+
try:
572+
round_num = int(changes_file.stem.split("_r")[1])
573+
changes_data = json.loads(changes_file.read_text())
574+
modified_files = changes_data.get("modified_files", {})
575+
576+
# Update line counts for files that changed in this round
577+
for file_path, file_content in modified_files.items():
578+
if file_content:
579+
current_file_lines[file_path] = len(file_content.splitlines())
580+
581+
# Record line counts for all files in this round
582+
round_line_counts = {}
583+
for file_path in all_files_list:
584+
round_line_counts[file_path] = current_file_lines.get(file_path, 0)
585+
586+
player_line_counts[round_num] = round_line_counts
587+
588+
except (json.JSONDecodeError, KeyError, ValueError):
589+
continue
590+
591+
if player_line_counts:
592+
line_counts_by_round[player_name] = player_line_counts
593+
594+
return {"all_files": all_files_list, "line_counts_by_round": line_counts_by_round}
595+
530596

531597
app = Flask(__name__)
532598

@@ -583,6 +649,9 @@ def index():
583649
if trajectory:
584650
trajectories_by_round[round_num].append(trajectory)
585651

652+
# Get analysis data
653+
analysis_data = parser.analyze_line_counts()
654+
586655
# Get the full path of the selected folder
587656
selected_folder_path = str(folder_path)
588657

@@ -592,6 +661,7 @@ def index():
592661
selected_folder_path=selected_folder_path,
593662
metadata=metadata,
594663
trajectories_by_round=trajectories_by_round,
664+
analysis_data=analysis_data,
595665
)
596666

597667

@@ -887,4 +957,28 @@ def move_folder():
887957
return jsonify({"success": False, "error": str(e)})
888958

889959

960+
@app.route("/analysis/line-counts")
961+
def analysis_line_counts():
962+
"""Get line count analysis data for the current game"""
963+
selected_folder = request.args.get("folder")
964+
965+
if not selected_folder:
966+
return jsonify({"success": False, "error": "No folder specified"})
967+
968+
# Validate the selected folder exists and is a game folder
969+
logs_dir = LOG_BASE_DIR
970+
folder_path = logs_dir / selected_folder
971+
972+
if not folder_path.exists() or not is_game_folder(folder_path):
973+
return jsonify({"success": False, "error": "Invalid folder"})
974+
975+
try:
976+
parser = LogParser(folder_path)
977+
analysis_data = parser.analyze_line_counts()
978+
979+
return jsonify({"success": True, "data": analysis_data})
980+
except Exception as e:
981+
return jsonify({"success": False, "error": str(e)})
982+
983+
890984
# Use run_viewer.py to launch the application

codeclash/viewer/static/css/style.css

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1331,3 +1331,77 @@ summary:focus {
13311331
.move-dialog-buttons button:last-child:hover {
13321332
background-color: var(--bg-tertiary);
13331333
}
1334+
1335+
/* Analysis Section Styles */
1336+
.analysis-section {
1337+
margin-bottom: 2rem;
1338+
}
1339+
1340+
.line-analysis-container {
1341+
margin-top: 1rem;
1342+
}
1343+
1344+
.file-selector {
1345+
margin-bottom: 1.5rem;
1346+
display: flex;
1347+
align-items: center;
1348+
gap: 0.5rem;
1349+
}
1350+
1351+
.file-selector label {
1352+
font-weight: 500;
1353+
color: var(--text-primary);
1354+
}
1355+
1356+
.file-dropdown {
1357+
padding: 0.5rem;
1358+
border: 1px solid var(--border-color);
1359+
border-radius: 0.375rem;
1360+
background-color: var(--bg-primary);
1361+
color: var(--text-primary);
1362+
font-size: 0.875rem;
1363+
min-width: 300px;
1364+
max-width: 100%;
1365+
}
1366+
1367+
.file-dropdown:focus {
1368+
outline: none;
1369+
border-color: var(--accent-color);
1370+
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
1371+
}
1372+
1373+
[data-theme="dark"] .file-dropdown:focus {
1374+
box-shadow: 0 0 0 0.2rem rgba(77, 171, 247, 0.25);
1375+
}
1376+
1377+
.chart-container {
1378+
position: relative;
1379+
height: 400px;
1380+
margin: 1rem 0;
1381+
background-color: var(--bg-primary);
1382+
border: 1px solid var(--border-color);
1383+
border-radius: 0.375rem;
1384+
padding: 1rem;
1385+
}
1386+
1387+
.chart-container canvas {
1388+
max-width: 100%;
1389+
max-height: 100%;
1390+
}
1391+
1392+
/* Responsive design for analysis */
1393+
@media (max-width: 768px) {
1394+
.file-selector {
1395+
flex-direction: column;
1396+
align-items: flex-start;
1397+
}
1398+
1399+
.file-dropdown {
1400+
min-width: 100%;
1401+
}
1402+
1403+
.chart-container {
1404+
height: 300px;
1405+
padding: 0.5rem;
1406+
}
1407+
}

codeclash/viewer/templates/index.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
{% include 'includes/readme.html' %}
1616
{% include 'includes/setup.html' %}
1717
{% include 'includes/overview.html' %}
18+
{% include 'includes/analysis.html' %}
1819
{% include 'includes/rounds.html' %}
1920
</main>
2021

@@ -34,6 +35,9 @@
3435
<button class="toc-item" onclick="scrollToElement('.overview-section')">
3536
📊 Overview
3637
</button>
38+
<button class="toc-item" onclick="scrollToElement('.analysis-section')">
39+
📈 Analysis
40+
</button>
3741
<button class="toc-item" onclick="scrollToElement('.rounds-section')">
3842
🎯 Rounds
3943
</button>
@@ -66,11 +70,13 @@ <h3>Move/Rename Folder</h3>
6670
</div>
6771

6872
<script src="https://unpkg.com/jsoneditor@latest/dist/jsoneditor.min.js"></script>
73+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
6974
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
7075
<script src="{{ url_for('static', filename='js/clipboard.js') }}"></script>
7176
<script src="{{ url_for('static', filename='js/experiment.js') }}"></script>
7277
<script src="{{ url_for('static', filename='js/readme.js') }}"></script>
7378
<script src="{{ url_for('static', filename='js/json-editors.js') }}"></script>
79+
<script src="{{ url_for('static', filename='js/analysis.js') }}"></script>
7480
<script>
7581
// Initialize all components when page loads
7682
document.addEventListener('DOMContentLoaded', function() {

0 commit comments

Comments
 (0)