2"""Enforce that every eligible C function in src/ is profiled and closes its timer on every return.
4Rules (docs/pages/09_Monitor_Reference.md, profiling):
5- every function defined in src/*.c that is not `static inline` and is not on the exclusion
6 list below begins exactly one timer, PROFILE_FUNCTION_BEGIN or PROFILE_HELPER_BEGIN;
7- every explicit `return` and `PetscFunctionReturn`/`PetscFunctionReturnVoid` after the timer's
8 BEGIN is immediately preceded by PROFILE_FUNCTION_END or PROFILE_HELPER_END, and a function whose
9 body can fall off its end ends with one; error returns inside SETERRQ/PetscCall/CHKERRQ are
10 not explicit returns and are exempt;
11- no file defines or reads `__FUNCT__`: timers, logging and PETSc's error traces all take the
14A missed END leaves the function's PETSc event open, after which its calls are no longer recorded.
17from __future__
import annotations
21from pathlib
import Path
23REPO_ROOT = Path(__file__).resolve().parents[2]
24SOURCES = sorted((REPO_ROOT /
"src").glob(
"*.c"))
28 "main":
"process entry; runs before PetscInitialize and after PetscFinalize",
29 "RuntimeShutdownSignalHandler":
"asynchronous signal handler; PETSc calls are not async-signal-safe",
30 "get_log_level":
"read by every logging macro; a timer would tax every log site",
31 "is_function_allowed":
"read by every logging macro that passes the level check",
32 "set_allowed_functions":
"configures logging before the profiler exists",
33 "_ProfilingRegister":
"the profiler itself",
34 "_ProfilingEnsureStarted":
"the profiler itself",
35 "_ProfilingNameListed":
"the profiler itself",
36 "_ProfilingHelperEnabled":
"the profiler itself",
37 "_ProfilingIsPicurvClass":
"the profiler itself",
38 "_ProfilingEventTotals":
"the profiler itself",
39 "_ProfilingSnapshotStep":
"the profiler itself",
40 "_ProfilingReduceSection":
"the profiler itself",
41 "_CompareNames":
"qsort comparator inside the profiler's report",
42 "_CompareProfiledFunctionRanks":
"qsort comparator inside the profiler's report",
43 "ProfilingInitialize":
"the profiler itself",
44 "ProfilingResetTimestepCounters":
"the profiler itself",
45 "ProfilingLogTimestepSummary":
"the profiler itself",
46 "ProfilingFinalize":
"the profiler itself; it reads every event's totals",
51 "src/checksum.c":
"SHA-256 and digest byte helpers; a leaf module with no PICurv headers",
52 "src/statistics_moments.c":
"per-sample moment arithmetic called per cell per step; a leaf module with no PICurv headers",
55BEGIN_RE = re.compile(
r"\bPROFILE_(?:FUNCTION|HELPER)_BEGIN\b")
56END_TAIL_RE = re.compile(
r"PROFILE_(?:FUNCTION|HELPER)_END\s*;\s*$")
57RETURN_RE = re.compile(
r"\b(?:return\b|PetscFunctionReturn(?:Void)?\s*\()")
58FUNCT_RE = re.compile(
r"\b__FUNCT__\b")
59CONTROL_WORDS = {
"if",
"for",
"while",
"switch",
"return",
"sizeof",
"do",
"else"}
64 @brief Replace comments, string and character literals with spaces, keeping offsets and newlines.
65 @param[in] text C source.
66 @return Text of the same length in which only code remains.
72 if text.startswith(
"//", i):
73 j = text.find(
"\n", i)
78 elif text.startswith(
"/*", i):
79 j = text.find(
"*/", i + 2)
80 j = n
if j < 0
else j + 2
87 while j < n
and text[j] != c:
88 j += 2
if text[j] ==
"\\" else 1
89 for k
in range(i + 1, min(j, n)):
93 elif c ==
"#" and (i == 0
or text[i - 1] ==
"\n"):
96 e = text.find(
"\n", j)
98 if e > 0
and text[e - 1] ==
"\\":
102 for k
in range(i, e):
112 @brief Locate top-level function definitions in comment- and string-blanked C code.
113 @param[in] code Output of blank_comments_and_strings().
114 @return List of (name, signature_start, body_open, body_close, is_static_inline).
117 depth, i, n = 0, 0, len(code)
123 head = code[stmt_start:i]
124 m = re.search(
r"\b([A-Za-z_]\w*)\s*\(([^;{}]*)\)\s*$", head, re.S)
125 if m
and m.group(1)
not in CONTROL_WORDS
and not re.search(
r"[=]\s*$", head.split(
"(")[0]):
128 d += (code[j] ==
"{") - (code[j] ==
"}")
130 sig = head[: m.start()]
131 words = set(re.findall(
r"\b\w+\b", sig))
132 functions.append((m.group(1), stmt_start, i, j - 1,
"static" in words
and "inline" in words))
140 elif c ==
";" and depth == 0:
148 @brief Check one function body against the timer rules.
149 @param[in] code Blanked source.
150 @param[in] body_open Offset of the body's opening brace.
151 @param[in] body_close Offset of the body's closing brace.
152 @return List of problem strings with source offsets.
154 body = code[body_open + 1 : body_close]
156 begins = len(BEGIN_RE.findall(body))
158 problems.append((body_open, f
"has {begins} timer BEGIN macros, expected 1"))
160 timer_start = BEGIN_RE.search(body).start()
161 for m
in RETURN_RE.finditer(body):
162 if m.start() < timer_start:
164 before = body[: m.start()].rstrip()
165 if not END_TAIL_RE.search(before):
166 problems.append((body_open + 1 + m.start(),
"return not preceded by PROFILE_*_END"))
167 if not ends_in_return(body)
and not END_TAIL_RE.search(body.rstrip()):
168 problems.append((body_close,
"body can fall off its end without PROFILE_*_END"))
174 @brief Whether a function body's last top-level statement is a return.
175 @details A body ending in a block (`}`) or in any other statement can fall off its end.
176 @param[in] body Blanked function body, without its braces.
177 @return True when the final statement returns.
180 if not tail.endswith(
";"):
183 for i, c
in enumerate(tail[:-1]):
188 if c ==
"}" and depth == 0:
190 elif c ==
";" and depth == 0:
192 return bool(RETURN_RE.match(tail[start:].lstrip()))
197 @brief 1-based line number of an offset.
198 @param[in] text Source text.
199 @param[in] offset Character offset.
202 return text.count(
"\n", 0, offset) + 1
207 @brief Run the audit over src/*.c, include/*.h and tests/c, print problems.
208 @return 0 when clean, 1 otherwise.
213 text = path.read_text(encoding=
"utf-8")
215 rel = path.relative_to(REPO_ROOT)
216 if str(rel)
in EXCLUDED_FILES:
218 for name, _, body_open, body_close, static_inline
in find_functions(code):
220 if static_inline
or name
in EXCLUDED:
223 problems.append(f
"{rel}:{line_of(text, offset)}: {name}: {msg}")
224 for path
in sorted((REPO_ROOT /
"src").glob(
"*.c")) + sorted((REPO_ROOT /
"include").glob(
"*.h")) + sorted((REPO_ROOT /
"tests" /
"c").glob(
"*.[ch]")):
225 text = path.read_text(encoding=
"utf-8")
227 problems.append(f
"{path.relative_to(REPO_ROOT)}:{line_of(text, m.start())}: uses __FUNCT__; use __func__")
228 stale = sorted(set(EXCLUDED) - seen)
230 problems.append(f
"tests/tooling/audit_profiling_coverage.py: excluded function '{name}' no longer exists")
234 print(f
"\nFound {len(problems)} profiling coverage issue(s).", file=sys.stderr)
236 print(
"Profiling coverage audit passed: every eligible src/ function times itself and closes its timer.")
240if __name__ ==
"__main__":
find_functions(str code)
Locate top-level function definitions in comment- and string-blanked C code.
bool ends_in_return(str body)
Whether a function body's last top-level statement is a return.
str blank_comments_and_strings(str text)
Replace comments, string and character literals with spaces, keeping offsets and newlines.
int main()
Run the audit over src/*.c, include/*.h and tests/c, print problems.
check_function(str code, int body_open, int body_close)
Check one function body against the timer rules.
int line_of(str text, int offset)
1-based line number of an offset.
Head of a generic C-style linked list.