Skip to content

Commit 3d131c6

Browse files
committed
checks: add code for the checks stats/dashboard
See the README.checks.rst for more info. Signed-off-by: Jakub Kicinski <[email protected]>
1 parent a79309d commit 3d131c6

File tree

5 files changed

+704
-0
lines changed

5 files changed

+704
-0
lines changed

README.checks.rst

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
======================
2+
Checks stats/dashboard
3+
======================
4+
5+
Checks dashboard generates a webpage with some stats about the checks
6+
which are failing most often.
7+
8+
Fetcher
9+
-------
10+
11+
``check_fetcher.py`` downloads the state of the checks from patchwork.
12+
Note that this means the status is fetched from where ``pw_upload.py``
13+
uploaded the results, not by consulting local file system.
14+
Fetcher dumps the state of checks as a flat array of objects -
15+
one object for each reported check (that is to say the state of
16+
patches and checks is "flattened" together). Entry format:
17+
18+
.. code-block:: json
19+
20+
{
21+
"id": 13372082,
22+
"date": "2023-09-01T06:21:27",
23+
"author": "Some Person",
24+
"state": "rfc",
25+
"delegate": "netdev",
26+
"check": "checkpatch",
27+
"result": "success",
28+
"description": "total: 0 errors, 0 warnings, 0 checks, 99 lines checked"
29+
},
30+
31+
Fetcher selects the checks by delegate so it will ignore all patches
32+
set to other delegate (currently hardcoded to "netdev").
33+
34+
Fetcher has to be run periodically (e.g. from systemd timer), it does
35+
one fetch and exists, it's not a daemon. It will write the results
36+
into a file called ``checks.json`` in results directory.
37+
38+
Static site
39+
-----------
40+
41+
``checks.html`` is the static HTML site, it contains the outline
42+
which JavaScript then populates.
43+
44+
Renderer
45+
--------
46+
47+
``checks.js`` is where most logic happens. It loads the JSON dumped
48+
by fetcher with jQuery, analyzes it and loads the results into the page.
49+
It uses Chart.js for the charts.

check_fetcher.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env python
2+
# SPDX-License-Identifier: GPL-2.0
3+
4+
import configparser
5+
import datetime
6+
import json
7+
import os
8+
import aiohttp
9+
10+
from core import NIPA_DIR
11+
from core import Maintainers, Person
12+
from core import log, log_open_sec, log_end_sec, log_init
13+
from core import Tree
14+
from pw import Patchwork
15+
16+
17+
def load_old_db(tgt_json):
18+
# Returns DB, map[patch -> state]
19+
try:
20+
with open(tgt_json, "r") as fp:
21+
old_db = json.load(fp)
22+
except FileNotFoundError:
23+
return [], {}
24+
25+
old_pstate = {}
26+
for row in old_db:
27+
old_pstate[row["id"]] = row["state"]
28+
29+
return old_db, old_pstate
30+
31+
32+
def main():
33+
# Init state
34+
global config
35+
config = configparser.ConfigParser()
36+
config.read(['nipa.config', 'pw.config', 'checks.config'])
37+
38+
log_init(config.get('log', 'type', fallback='org'),
39+
config.get('log', 'file', fallback=os.path.join(NIPA_DIR, "checks.org")),
40+
force_single_thread=True)
41+
42+
rdir = config.get('dirs', 'results', fallback=os.path.join(NIPA_DIR, "results"))
43+
tgt_json = os.path.join(rdir, "checks.json")
44+
45+
# Time bounds
46+
retain_history_days = 60 # how much data we want in the JSON
47+
look_back_days = 8 # oldest patch we may change state of
48+
expect_checks_stable_hours = 50 # oldest patch where checks themselves may change
49+
delegate = "netdev"
50+
51+
pw = Patchwork(config)
52+
53+
old_db, old_pstate = load_old_db(tgt_json)
54+
55+
now = datetime.datetime.utcnow()
56+
since = now - datetime.timedelta(days=look_back_days)
57+
58+
json_resp = pw.get_patches_all(delegate=delegate, since=since)
59+
jdb = []
60+
old_unchanged = 0
61+
seen_pids = set()
62+
for p in json_resp:
63+
pdate = datetime.datetime.fromisoformat(p["date"])
64+
hours_old = (now - pdate).total_seconds() // 3600
65+
# Checks won't get updated after 2+ days, so if the state is the same - skip
66+
if hours_old > expect_checks_stable_hours and \
67+
p["id"] in old_pstate and p["state"] == old_pstate[p["id"]]:
68+
old_unchanged += 1
69+
continue
70+
71+
seen_pids.add(p["id"])
72+
checks = pw.request(p["checks"])
73+
for c in checks:
74+
info = {
75+
"id": p["id"],
76+
"date": p["date"],
77+
"author": p["submitter"]["name"],
78+
"state": p["state"],
79+
"delegate": p["delegate"]["username"],
80+
"check": c["context"],
81+
"result": c["state"],
82+
"description": c["description"]
83+
}
84+
jdb.append(info)
85+
86+
new_db = []
87+
skipped = 0
88+
horizon_gc = 0
89+
old_stayed = 0
90+
for row in old_db:
91+
pdate = datetime.datetime.fromisoformat(row["date"])
92+
days_old = (now - pdate).days
93+
if days_old > retain_history_days:
94+
horizon_gc += 1
95+
continue
96+
if row["id"] in seen_pids:
97+
skipped += 1
98+
continue
99+
old_stayed += 1
100+
new_db.append(row)
101+
new_db += jdb
102+
print(f'Old db: {len(old_db)}, retained: {old_stayed}')
103+
print(f'Fetching: patches: {len(json_resp)}, patches old-unchanged: {old_unchanged}, checks fetched: {len(jdb)}')
104+
print(f'Writing: refreshed: {skipped}, new: {len(new_db) - old_stayed}, expired: {horizon_gc} new len: {len(new_db)}')
105+
106+
with open(tgt_json, "w") as fp:
107+
json.dump(new_db, fp)
108+
109+
110+
if __name__ == "__main__":
111+
main()

checks.html

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>PW check stats</title>
6+
<link rel="shortcut icon" href="/favicon-stats.png" type="image/png" />
7+
8+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
9+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
10+
<script src="checks.js"></script>
11+
<script>
12+
do_it();
13+
</script>
14+
<style>
15+
table {
16+
font-family: arial, sans-serif;
17+
border-collapse: collapse;
18+
width: 100%;
19+
}
20+
21+
td, th {
22+
border: 1px solid #eeeeee;
23+
text-align: left;
24+
padding: 8px;
25+
}
26+
27+
tr:nth-child(even) {
28+
background-color: #eeeeee;
29+
}
30+
.row {
31+
display: flex;
32+
}
33+
34+
.column {
35+
flex: 50%;
36+
}
37+
</style>
38+
</head>
39+
<body>
40+
<div class="row">
41+
<div class="column">
42+
<h3>Patch color (accepted)</h3>
43+
<canvas id="gyr_accept"></canvas>
44+
</div>
45+
<div class="column">
46+
<h3>Patch color (all)</h3>
47+
<canvas id="gyr_all"></canvas>
48+
</div>
49+
</div>
50+
<div class="row">
51+
<div class="column">
52+
<h3>Check fail rate (accepted)</h3>
53+
<canvas id="pc_accept"></canvas>
54+
</div>
55+
<div class="column">
56+
<h3>Check fail rate (all)</h3>
57+
<canvas id="pc_all"></canvas>
58+
</div>
59+
</div>
60+
61+
62+
<div class="row">
63+
<div class="column">
64+
<h3>Avg fail rate (accepted)</h3>
65+
<table id="avg_rate_accept">
66+
<thead>
67+
<tr>
68+
<th></th>
69+
<th colspan="3">2 Weeks</th>
70+
<th colspan="3">All time</th>
71+
</tr>
72+
<tr>
73+
<th>Check</th>
74+
<th>Success</th>
75+
<th>Warning</th>
76+
<th>Fail</th>
77+
<th>Success</th>
78+
<th>Warning</th>
79+
<th>Fail</th>
80+
</tr>
81+
<thead>
82+
</table>
83+
</div>
84+
<div class="column">
85+
<h3>Avg fail rate (all)</h3>
86+
<table id="avg_rate_all">
87+
<thead>
88+
<tr>
89+
<th></th>
90+
<th colspan="3">2 Weeks</th>
91+
<th colspan="3">All time</th>
92+
</tr>
93+
<tr>
94+
<th>Check</th>
95+
<th>Success</th>
96+
<th>Warning</th>
97+
<th>Fail</th>
98+
<th>Success</th>
99+
<th>Warning</th>
100+
<th>Fail</th>
101+
</tr>
102+
<thead>
103+
</table>
104+
</div>
105+
</div>
106+
107+
<div class="row">
108+
<div class="column">
109+
<h3>Top 15 developers failing checks (patch applied anyway)</h3>
110+
<table id="person_accept">
111+
<tr>
112+
<th></th>
113+
<th>Person</th>
114+
<th>Fails</th>
115+
<th>Warnings</th>
116+
<th>Total</th>
117+
</tr>
118+
</table>
119+
</div>
120+
<div class="column">
121+
<h3>Top 15 developers failing checks</h3>
122+
<table id="person_all">
123+
<tr>
124+
<th></th>
125+
<th>Person</th>
126+
<th>Fails</th>
127+
<th>Warnings</th>
128+
<th>Total</th>
129+
</tr>
130+
</table>
131+
</div>
132+
</div>
133+
134+
<div class="row">
135+
<div class="column">
136+
<h3>Top 20 check outputs</h3>
137+
<table id="logTable">
138+
<tr>
139+
<th>Check</th>
140+
<th>Output</th>
141+
<th>Hits</th>
142+
</tr>
143+
</table>
144+
</div>
145+
<div class="column">
146+
</div>
147+
</div>
148+
</body>
149+
</html>

0 commit comments

Comments
 (0)