Skip to content

Commit 9aa99c3

Browse files
authored
Track metrics for core and std crates, add loop metrics (#258)
The crate that we compute metrics for is now configurable when invoking `kani_std_analysis.py`. Should we find a need for further crates beyond `core` or `std` we just need to invoke it more times. We are now also reporting loop/no-loop information across all the kinds of functions we are tracking as metrics. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.
1 parent 18c4e95 commit 9aa99c3

File tree

6 files changed

+174
-38
lines changed

6 files changed

+174
-38
lines changed

doc/mdbook-metrics/src/main.rs

+27-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,25 @@ fn run_kani_metrics_script() -> Result<(), Error> {
4444
Command::new("python")
4545
.args(&[
4646
"kani_std_analysis.py",
47+
"--crate",
48+
"core",
49+
"--metrics-file",
50+
"metrics-data-core.json",
51+
"--plot-only",
52+
"--plot-dir",
53+
&tools_path.to_string_lossy(),
54+
])
55+
.stdout(std::process::Stdio::null())
56+
.stderr(std::process::Stdio::null())
57+
.status()?;
58+
59+
Command::new("python")
60+
.args(&[
61+
"kani_std_analysis.py",
62+
"--crate",
63+
"std",
64+
"--metrics-file",
65+
"metrics-data-std.json",
4766
"--plot-only",
4867
"--plot-dir",
4968
&tools_path.to_string_lossy(),
@@ -83,12 +102,19 @@ fn add_graphs(chapter: &mut Chapter) {
83102
84103
Note that these metrics are for x86-64 architectures.
85104
86-
## `core`
105+
### `core`
87106
![Unsafe Metrics](core_unsafe_metrics.png)
88107
89108
![Safe Abstractions Metrics](core_safe_abstractions_metrics.png)
90109
91110
![Safe Metrics](core_safe_metrics.png)
111+
112+
### `std`
113+
![Unsafe Metrics](std_unsafe_metrics.png)
114+
115+
![Safe Abstractions Metrics](std_safe_abstractions_metrics.png)
116+
117+
![Safe Metrics](std_safe_metrics.png)
92118
"#;
93119

94120
chapter.content.push_str(new_content);

scripts/kani-std-analysis/kani_std_analysis.py

+137-35
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,38 @@
2727
# But `kani list` runs on this fork, so it can still see it and add it to the total functions under contract.
2828
# - See #TODOs for known limitations.
2929

30+
def str_to_bool(string: str):
31+
match string.strip().lower():
32+
case "true":
33+
return True
34+
case "false":
35+
return False
36+
case _:
37+
print(f"Unexpected to-be-Boolean string {string}")
38+
sys.exit(1)
39+
40+
3041
# Process the results from Kani's std-analysis.sh script for each crate.
31-
# TODO For now, we just handle "core", but we should process all crates in the library.
3242
class GenericSTDMetrics():
33-
def __init__(self, results_dir):
43+
def __init__(self, results_dir, crate):
3444
self.results_directory = results_dir
45+
self.crate = crate
3546
self.unsafe_fns_count = 0
3647
self.safe_abstractions_count = 0
3748
self.safe_fns_count = 0
3849
self.unsafe_fns = []
50+
self.unsafe_fns_with_loop = []
3951
self.safe_abstractions = []
52+
self.safe_abstractions_with_loop = []
4053
self.safe_fns = []
54+
self.safe_fns_with_loop = []
4155

4256
self.read_std_analysis()
4357

4458
# Read {crate}_overall_counts.csv
4559
# and return the number of unsafe functions and safe abstractions
4660
def read_overall_counts(self):
47-
file_path = f"{self.results_directory}/core_scan_overall.csv"
61+
file_path = f"{self.results_directory}/{self.crate}_scan_overall.csv"
4862
with open(file_path, 'r') as f:
4963
csv_reader = csv.reader(f, delimiter=';')
5064
counts = {row[0]: int(row[1]) for row in csv_reader if len(row) >= 2}
@@ -55,54 +69,60 @@ def read_overall_counts(self):
5569
# Read {crate}_scan_functions.csv
5670
# and return an array of the unsafe functions and the safe abstractions
5771
def read_scan_functions(self):
58-
expected_header_start = "name;is_unsafe;has_unsafe_ops"
59-
file_path = f"{self.results_directory}/core_scan_functions.csv"
72+
expected_header_start = "name;is_unsafe;has_unsafe_ops;has_unsupported_input;has_loop"
73+
file_path = f"{self.results_directory}/{self.crate}_scan_functions.csv"
6074

6175
with open(file_path, 'r') as f:
6276
csv_reader = csv.reader(f, delimiter=';', quotechar='"')
6377

6478
# The row parsing logic below assumes the column structure in expected_header_start,
6579
# so assert that is how the header begins before continuing
6680
header = next(csv_reader)
67-
header_str = ';'.join(header[:3])
81+
header_str = ';'.join(header[:5])
6882
if not header_str.startswith(expected_header_start):
6983
print(f"Error: Unexpected CSV header in {file_path}")
7084
print(f"Expected header to start with: {expected_header_start}")
7185
print(f"Actual header: {header_str}")
7286
sys.exit(1)
7387

7488
for row in csv_reader:
75-
if len(row) >= 3:
89+
if len(row) >= 5:
7690
name, is_unsafe, has_unsafe_ops = row[0], row[1], row[2]
91+
has_unsupported_input, has_loop = row[3], row[4]
7792
# An unsafe function is a function for which is_unsafe=true
78-
if is_unsafe.strip() == "true":
93+
if str_to_bool(is_unsafe):
7994
self.unsafe_fns.append(name)
95+
if str_to_bool(has_loop):
96+
self.unsafe_fns_with_loop.append(name)
8097
else:
81-
assert is_unsafe.strip() == "false" # sanity check against malformed data
8298
self.safe_fns.append(name)
99+
if str_to_bool(has_loop):
100+
self.safe_fns_with_loop.append(name)
83101
# A safe abstraction is a safe function with unsafe ops
84-
if has_unsafe_ops.strip() == "true":
102+
if str_to_bool(has_unsafe_ops):
85103
self.safe_abstractions.append(name)
104+
if str_to_bool(has_loop):
105+
self.safe_abstractions_with_loop.append(name)
86106

87107
def read_std_analysis(self):
88108
self.read_overall_counts()
89109
self.read_scan_functions()
90110

91111
# Sanity checks
92112
if len(self.unsafe_fns) != self.unsafe_fns_count:
93-
print(f"Number of unsafe functions does not match core_scan_functions.csv")
113+
print(f"Number of unsafe functions does not match {self.crate}_scan_functions.csv")
94114
print(f"UNSAFE_FNS_COUNT: {self.unsafe_fns_count}")
95115
print(f"UNSAFE_FNS length: {len(self.unsafe_fns)}")
96116
sys.exit(1)
97117

98118
if len(self.safe_abstractions) != self.safe_abstractions_count:
99-
print(f"Number of safe abstractions does not match core_scan_functions.csv")
119+
print(f"Number of safe abstractions does not match {self.crate}_scan_functions.csv")
100120
print(f"SAFE_ABSTRACTIONS_COUNT: {self.safe_abstractions_count}")
101121
print(f"SAFE_ABSTRACTIONS length: {len(self.safe_abstractions)}")
102122
sys.exit(1)
103123

104124
if len(self.safe_fns) != self.safe_fns_count:
105-
print(f"Number of safe functions does not match core_scan_functions.csv")
125+
print(f"Number of safe functions does not match {self.crate}_scan_functions.csv")
106126
print(f"SAFE_FNS_COUNT: {self.safe_fns_count}")
107127
print(f"SAFE_FNS length: {len(self.safe_fns)}")
108128
sys.exit(1)
@@ -140,11 +160,33 @@ def read_kani_list_data(self, kani_list_filepath):
140160
# Generate metrics about Kani's application to the standard library over time
141161
# by reading past metrics from metrics_file, then computing today's metrics.
142162
class KaniSTDMetricsOverTime():
143-
def __init__(self, metrics_file):
163+
def __init__(self, metrics_file, crate):
164+
self.crate = crate
144165
self.dates = []
145-
self.unsafe_metrics = ['total_unsafe_fns', 'unsafe_fns_under_contract', 'verified_unsafe_fns_under_contract']
146-
self.safe_abstr_metrics = ['total_safe_abstractions', 'safe_abstractions_under_contract', 'verified_safe_abstractions_under_contract']
147-
self.safe_metrics = ['total_safe_fns', 'safe_fns_under_contract', 'verified_safe_fns_under_contract']
166+
self.unsafe_metrics = [
167+
'total_unsafe_fns',
168+
'total_unsafe_fns_with_loop',
169+
'unsafe_fns_under_contract',
170+
'unsafe_fns_with_loop_under_contract',
171+
'verified_unsafe_fns_under_contract',
172+
'verified_unsafe_fns_with_loop_under_contract'
173+
]
174+
self.safe_abstr_metrics = [
175+
'total_safe_abstractions',
176+
'total_safe_abstractions_with_loop',
177+
'safe_abstractions_under_contract',
178+
'safe_abstractions_with_loop_under_contract',
179+
'verified_safe_abstractions_under_contract',
180+
'verified_safe_abstractions_with_loop_under_contract'
181+
]
182+
self.safe_metrics = [
183+
'total_safe_fns',
184+
'total_safe_fns_with_loop',
185+
'safe_fns_under_contract',
186+
'safe_fns_with_loop_under_contract',
187+
'verified_safe_fns_under_contract',
188+
'verified_safe_fns_with_loop_under_contract'
189+
]
148190
# The keys in these dictionaries are unsafe_metrics, safe_abstr_metrics, and safe_metrics, respectively; see update_plot_metrics()
149191
self.unsafe_plot_data = defaultdict(list)
150192
self.safe_abstr_plot_data = defaultdict(list)
@@ -157,12 +199,9 @@ def __init__(self, metrics_file):
157199

158200
# Read historical data from self.metrics_file and initialize the date range.
159201
def read_historical_data(self):
160-
try:
161-
with open(self.metrics_file, 'r') as f:
162-
all_data = json.load(f)["results"]
163-
self.update_plot_metrics(all_data)
164-
except FileNotFoundError:
165-
all_data = {}
202+
with open(self.metrics_file, 'r') as f:
203+
all_data = json.load(f)["results"]
204+
self.update_plot_metrics(all_data)
166205

167206
self.dates = [datetime.strptime(data["date"], '%Y-%m-%d').date() for data in all_data]
168207

@@ -173,15 +212,15 @@ def update_plot_metrics(self, all_data):
173212
for data in all_data:
174213
for metric in self.unsafe_metrics:
175214
if not metric.startswith("verified"):
176-
self.unsafe_plot_data[metric].append(data[metric])
215+
self.unsafe_plot_data[metric].append(data.get(metric, 0))
177216

178217
for metric in self.safe_abstr_metrics:
179218
if not metric.startswith("verified"):
180-
self.safe_abstr_plot_data[metric].append(data[metric])
219+
self.safe_abstr_plot_data[metric].append(data.get(metric, 0))
181220

182221
for metric in self.safe_metrics:
183222
if not metric.startswith("verified"):
184-
self.safe_plot_data[metric].append(data[metric])
223+
self.safe_plot_data[metric].append(data.get(metric, 0))
185224

186225
# Read output from kani list and std-analysis.sh, then compare their outputs to compute Kani-specific metrics
187226
# and write the results to {self.metrics_file}
@@ -190,41 +229,68 @@ def compute_metrics(self, kani_list_filepath, analysis_results_dir):
190229

191230
# Process the `kani list` and `std-analysis.sh` data
192231
kani_data = KaniListSTDMetrics(kani_list_filepath)
193-
generic_metrics = GenericSTDMetrics(analysis_results_dir)
232+
generic_metrics = GenericSTDMetrics(analysis_results_dir, self.crate)
194233

195234
print("Comparing kani-list output to std-analysis.sh output and computing metrics...")
196235

197236
(unsafe_fns_under_contract, verified_unsafe_fns_under_contract) = (0, 0)
237+
unsafe_fns_with_loop_under_contract = 0
238+
verified_unsafe_fns_with_loop_under_contract = 0
198239
(safe_abstractions_under_contract, verified_safe_abstractions_under_contract) = (0, 0)
240+
safe_abstractions_with_loop_under_contract = 0
241+
verified_safe_abstractions_with_loop_under_contract = 0
199242
(safe_fns_under_contract, verified_safe_fns_under_contract) = (0, 0)
243+
safe_fns_with_loop_under_contract = 0
244+
verified_safe_fns_with_loop_under_contract = 0
200245

201246
for (func_under_contract, has_harnesses) in kani_data.fns_under_contract:
202247
if func_under_contract in generic_metrics.unsafe_fns:
248+
has_loop = int(func_under_contract in
249+
generic_metrics.unsafe_fns_with_loop)
203250
unsafe_fns_under_contract += 1
251+
unsafe_fns_with_loop_under_contract += has_loop
204252
if has_harnesses:
205253
verified_unsafe_fns_under_contract += 1
254+
verified_unsafe_fns_with_loop_under_contract += has_loop
206255
if func_under_contract in generic_metrics.safe_abstractions:
256+
has_loop = int(func_under_contract in
257+
generic_metrics.safe_abstractions_with_loop)
207258
safe_abstractions_under_contract += 1
259+
safe_abstractions_with_loop_under_contract += has_loop
208260
if has_harnesses:
209261
verified_safe_abstractions_under_contract += 1
262+
verified_safe_abstractions_with_loop_under_contract += has_loop
210263
if func_under_contract in generic_metrics.safe_fns:
264+
has_loop = int(func_under_contract in
265+
generic_metrics.safe_fns_with_loop)
211266
safe_fns_under_contract += 1
267+
safe_fns_with_loop_under_contract += has_loop
212268
if has_harnesses:
213269
verified_safe_fns_under_contract += 1
270+
verified_safe_fns_with_loop_under_contract += has_loop
214271

215272
# Keep the keys here in sync with unsafe_metrics, safe_metrics, and safe_abstr_metrics
216273
data = {
217274
"date": self.date,
218275
"total_unsafe_fns": generic_metrics.unsafe_fns_count,
276+
"total_unsafe_fns_with_loop": len(generic_metrics.unsafe_fns_with_loop),
219277
"total_safe_abstractions": generic_metrics.safe_abstractions_count,
278+
"total_safe_abstractions_with_loop": len(generic_metrics.safe_abstractions_with_loop),
220279
"total_safe_fns": generic_metrics.safe_fns_count,
280+
"total_safe_fns_with_loop": len(generic_metrics.safe_fns_with_loop),
221281
"unsafe_fns_under_contract": unsafe_fns_under_contract,
282+
"unsafe_fns_with_loop_under_contract": unsafe_fns_with_loop_under_contract,
222283
"verified_unsafe_fns_under_contract": verified_unsafe_fns_under_contract,
284+
"verified_unsafe_fns_with_loop_under_contract": verified_unsafe_fns_with_loop_under_contract,
223285
"safe_abstractions_under_contract": safe_abstractions_under_contract,
286+
"safe_abstractions_with_loop_under_contract": safe_abstractions_with_loop_under_contract,
224287
"verified_safe_abstractions_under_contract": verified_safe_abstractions_under_contract,
288+
"verified_safe_abstractions_with_loop_under_contract": verified_safe_abstractions_with_loop_under_contract,
225289
"safe_fns_under_contract": safe_fns_under_contract,
290+
"safe_fns_with_loop_under_contract": safe_fns_with_loop_under_contract,
226291
"verified_safe_fns_under_contract": verified_safe_fns_under_contract,
227-
"total_functions_under_contract": kani_data.total_fns_under_contract,
292+
"verified_safe_fns_with_loop_under_contract": verified_safe_fns_with_loop_under_contract,
293+
"total_functions_under_contract_all_crates": kani_data.total_fns_under_contract,
228294
}
229295

230296
self.update_plot_metrics([data])
@@ -243,7 +309,27 @@ def compute_metrics(self, kani_list_filepath, analysis_results_dir):
243309
def plot_single(self, data, title, filename, plot_dir):
244310
plt.figure(figsize=(14, 8))
245311

246-
colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#946F7bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
312+
colors = [
313+
'#1f77b4', #total_unsafe_fns
314+
'#941fb4', #total_unsafe_fns_with_loop
315+
'#ff7f0e', #total_safe_abstractions
316+
'#abff0e', #total_safe_abstractions_with_loop
317+
'#2ca02c', #total_safe_fns
318+
'#a02c8d', #total_safe_fns_with_loop
319+
'#d62728', #unsafe_fns_under_contract
320+
'#27d6aa', #unsafe_fns_with_loop_under_contract
321+
'#9467bd', #verified_unsafe_fns_under_contract
322+
'#67acbd', #verified_unsafe_fns_with_loop_under_contract
323+
'#8c564b', #safe_abstractions_under_contract
324+
'#8c814b', #safe_abstractions_with_loop_under_contract
325+
'#e377c2', #verified_safe_abstractions_under_contract
326+
'#a277e3', #verified_safe_abstractions_with_loop_under_contract
327+
'#7f7f7f', #safe_fns_under_contract
328+
'#9e6767', #safe_fns_with_loop_under_contract
329+
'#bcbd22', #verified_safe_fns_under_contract
330+
'#49bd22', #verified_safe_fns_with_loop_under_contract
331+
'#17becf' #total_functions_under_contract
332+
]
247333

248334
for i, (metric, values) in enumerate(data.items()):
249335
color = colors[i % len(colors)]
@@ -280,16 +366,32 @@ def plot_single(self, data, title, filename, plot_dir):
280366
print(f"PNG graph generated: {outfile}")
281367

282368
def plot(self, plot_dir):
283-
self.plot_single(self.unsafe_plot_data, title="Contracts on Unsafe Functions in core", filename="core_unsafe_metrics.png", plot_dir=plot_dir)
284-
self.plot_single(self.safe_abstr_plot_data, title="Contracts on Safe Abstractions in core", filename="core_safe_abstractions_metrics.png", plot_dir=plot_dir)
285-
self.plot_single(self.safe_plot_data, title="Contracts on Safe Functions in core", filename="core_safe_metrics.png", plot_dir=plot_dir)
369+
self.plot_single(
370+
self.unsafe_plot_data,
371+
title=f"Contracts on Unsafe Functions in {self.crate}",
372+
filename=f"{self.crate}_unsafe_metrics.png",
373+
plot_dir=plot_dir)
374+
self.plot_single(
375+
self.safe_abstr_plot_data,
376+
title=f"Contracts on Safe Abstractions in {self.crate}",
377+
filename=f"{self.crate}_safe_abstractions_metrics.png",
378+
plot_dir=plot_dir)
379+
self.plot_single(
380+
self.safe_plot_data,
381+
title=f"Contracts on Safe Functions in {self.crate}",
382+
filename=f"{self.crate}_safe_metrics.png",
383+
plot_dir=plot_dir)
286384

287385
def main():
288386
parser = argparse.ArgumentParser(description="Generate metrics about Kani's application to the standard library.")
387+
parser.add_argument('--crate',
388+
type=str,
389+
required=True,
390+
help="Name of standard library crate to produce metrics for")
289391
parser.add_argument('--metrics-file',
290392
type=str,
291-
default="metrics-data.json",
292-
help="Path to the JSON file containing metrics data (default: metrics-data.json)")
393+
required=True,
394+
help="Path to the JSON file containing metrics data")
293395
parser.add_argument('--kani-list-file',
294396
type=str,
295397
default="kani-list.json",
@@ -308,7 +410,7 @@ def main():
308410

309411
args = parser.parse_args()
310412

311-
metrics = KaniSTDMetricsOverTime(args.metrics_file)
413+
metrics = KaniSTDMetricsOverTime(args.metrics_file, args.crate)
312414

313415
if args.plot_only:
314416
metrics.plot(args.plot_dir)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"results": [
3+
]
4+
}

scripts/kani-std-analysis/std-analysis.sh

-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ then
6565
fi
6666
cargo new std_lib_analysis --lib
6767
cd std_lib_analysis
68-
sed -i '1i cargo-features = ["edition2024"]' Cargo.toml
6968

7069
echo '
7170
pub fn dummy() {

0 commit comments

Comments
 (0)