Skip to content

Commit 3a52448

Browse files
authored
gh_parse.py: show slowest test durations, total time (#3903)
## Changes Include total time, average time, slowest test per env and top 50 slowest tests in the integration test report. ## Why Better understanding which integration tests / envs take most time. ## Tests See report on this PR: #3903 (comment)
1 parent 041207d commit 3a52448

File tree

1 file changed

+82
-3
lines changed

1 file changed

+82
-3
lines changed

tools/gh_parse.py

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@
4040
INTERESTING_ACTIONS = (PANIC, BUG, FAIL, KNOWN_FAILURE, MISSING, FLAKY, RECOVERED, KNOWN_SKIP)
4141
ACTIONS_WITH_ICON = INTERESTING_ACTIONS + (PASS, SKIP)
4242

43+
# Minimum elapsed time for test to be in slowest tests table.
44+
SLOWEST_MIN_MINUTES = 2
45+
46+
# Maximum number of tests in slowest tests table
47+
SLOWEST_MAX_ENTRIES = 50
48+
4349
ACTION_MESSAGES = {
4450
"fail": FAIL,
4551
"pass": PASS,
@@ -277,6 +283,8 @@ def iter_paths(paths):
277283
def parse_file(path, filter):
278284
results = {}
279285
outputs = {}
286+
durations = {} # test_key -> elapsed time in seconds
287+
timestamps = [] # list of all timestamps from this file
280288
for line in path.open():
281289
if not line.strip():
282290
continue
@@ -285,6 +293,12 @@ def parse_file(path, filter):
285293
except Exception as ex:
286294
print(f"{path}: {ex}\n{line!r}\n")
287295
break
296+
297+
# Collect timestamp
298+
timestamp = data.get("Time")
299+
if timestamp:
300+
timestamps.append(timestamp)
301+
288302
testname = data.get("Test")
289303
if not testname:
290304
continue
@@ -304,6 +318,11 @@ def parse_file(path, filter):
304318
else:
305319
results[test_key] = action
306320

321+
# Store elapsed time if available
322+
elapsed = data.get("Elapsed")
323+
if elapsed and action == PASS:
324+
durations[test_key] = elapsed
325+
307326
out = data.get("Output")
308327
if out:
309328
outputs.setdefault(test_key, []).append(out.rstrip())
@@ -316,7 +335,7 @@ def parse_file(path, filter):
316335
else:
317336
results.setdefault(test_key, MISSING)
318337

319-
return results, outputs
338+
return results, outputs, durations, timestamps
320339

321340

322341
def mark_known_failures(results, known_failures_config):
@@ -338,6 +357,8 @@ def print_report(filenames, filter, filter_env, show_output, markdown=False, omi
338357
known_failures_config = load_known_failures()
339358
outputs = {} # test_key -> env -> [output]
340359
per_test_per_env_stats = {} # test_key -> env -> action -> count
360+
durations_by_env = {} # env -> test_key -> duration
361+
timestamps_by_env = {} # env -> list of timestamps
341362
all_test_keys = set()
342363
all_envs = set()
343364
count_files = 0
@@ -351,14 +372,17 @@ def print_report(filenames, filter, filter_env, show_output, markdown=False, omi
351372
if filter_env and filter_env not in env:
352373
continue
353374
all_envs.add(env)
354-
test_results, test_outputs = parse_file(p, filter)
375+
test_results, test_outputs, test_durations, test_timestamps = parse_file(p, filter)
355376
test_results = mark_known_failures(test_results, known_failures_config)
356377
count_files += 1
357378
count_results += len(test_results)
358379
for test_key, action in test_results.items():
359380
per_test_per_env_stats.setdefault(test_key, {}).setdefault(env, Counter())[action] += 1
360381
for test_key, output in test_outputs.items():
361382
outputs.setdefault(test_key, {}).setdefault(env, []).extend(output)
383+
for test_key, duration in test_durations.items():
384+
durations_by_env.setdefault(env, {})[test_key] = duration
385+
timestamps_by_env.setdefault(env, []).extend(test_timestamps)
362386
all_test_keys.update(test_results)
363387

364388
print(f"Parsed {count_files} files: {count_results} results", file=sys.stderr, flush=True)
@@ -420,10 +444,32 @@ def is_bug(test_results):
420444
status = action[:2]
421445
break
422446

447+
# Find slowest test, mean duration, and time span for this environment
448+
env_durations = durations_by_env.get(env, {})
449+
time_span = ""
450+
451+
# Calculate time span (max timestamp - min timestamp)
452+
env_timestamps = timestamps_by_env.get(env, [])
453+
if env_timestamps:
454+
from datetime import datetime
455+
456+
# Parse timestamps and find span
457+
parsed_timestamps = []
458+
for ts in env_timestamps:
459+
try:
460+
parsed_timestamps.append(datetime.fromisoformat(ts.replace("Z", "+00:00")))
461+
except Exception:
462+
continue
463+
464+
if parsed_timestamps:
465+
time_span_seconds = (max(parsed_timestamps) - min(parsed_timestamps)).total_seconds()
466+
time_span = format_duration(time_span_seconds)
467+
423468
table.append(
424469
{
425470
" ": status,
426471
"Env": env,
472+
"Time": time_span,
427473
**stats,
428474
}
429475
)
@@ -432,10 +478,12 @@ def is_bug(test_results):
432478
def key(column):
433479
try:
434480
return (ACTIONS_WITH_ICON.index(column), "")
435-
except:
481+
except Exception:
436482
return (-1, str(column))
437483

438484
columns = sorted(columns, key=key)
485+
columns.append("Time")
486+
439487
print(format_table(table, markdown=markdown, columns=columns))
440488

441489
interesting_envs = set()
@@ -485,6 +533,28 @@ def key(column):
485533
if table_txt:
486534
print(table_txt)
487535

536+
# Generate slowest tests table (tests slower than 10 minutes)
537+
all_durations = [] # [(env, package, testname, duration), ...]
538+
for env, env_durations in durations_by_env.items():
539+
for test_key, duration in env_durations.items():
540+
package_name, testname = test_key
541+
if duration >= SLOWEST_MIN_MINUTES * 60:
542+
all_durations.append((env, package_name, testname, duration))
543+
544+
all_durations.sort(key=lambda x: x[3], reverse=True)
545+
top_slowest = all_durations[:SLOWEST_MAX_ENTRIES]
546+
547+
if top_slowest:
548+
slowest_table = []
549+
for env, package_name, testname, duration in top_slowest:
550+
slowest_table.append({"duration": format_duration(duration), "env": env, "testname": testname})
551+
552+
slowest_table_txt = format_table(slowest_table, columns=["duration", "env", "testname"], markdown=markdown)
553+
slowest_table_txt = wrap_in_details(
554+
slowest_table_txt, f"Top {len(top_slowest)} slowest tests (at least {SLOWEST_MIN_MINUTES} minutes):"
555+
)
556+
print(slowest_table_txt)
557+
488558
if show_output:
489559
for test_key, stats in simplified_results.items():
490560
package_name, testname = test_key
@@ -584,6 +654,15 @@ def wrap_in_details(txt, summary):
584654
return f"<details><summary>{summary}</summary>\n\n{txt}\n\n</details>"
585655

586656

657+
def format_duration(seconds):
658+
"""Format duration from seconds to MM:SS format."""
659+
if seconds is None:
660+
return ""
661+
minutes = int(seconds // 60)
662+
secs = int(seconds % 60)
663+
return f"{minutes}:{secs:02d}"
664+
665+
587666
def main():
588667
parser = argparse.ArgumentParser()
589668
parser.add_argument("filenames", nargs="+", help="Filenames or directories to parse")

0 commit comments

Comments
 (0)