Skip to content

Commit 3fc4df2

Browse files
authored
Incorporated Pydantic JSON schema checking (#858)
* Pydantic JSON schema checking. * Weakened type for now.
1 parent 7a012c6 commit 3fc4df2

File tree

2 files changed

+112
-2
lines changed

2 files changed

+112
-2
lines changed

pyproject.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ dependencies = [
4040
"Jinja2>=3.0.3",
4141
"psutil>=5.9.2",
4242
"numpy>=1.24.0,<1.27",
43-
"astunparse>=1.6.3; python_version < '3.9'"
43+
"astunparse>=1.6.3; python_version < '3.9'",
44+
"pydantic>=2.6",
4445
]
4546
dynamic = ["version"] # computed by setup.py
4647

scalene/scalene_json.py

+110-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import re
44

55
from collections import OrderedDict, defaultdict
6+
from enum import Enum
67
from operator import itemgetter
78
from pathlib import Path
9+
from pydantic import BaseModel, Field, NonNegativeFloat, NonNegativeInt, PositiveInt, StrictBool, ValidationError, confloat, model_validator
810
from typing import Any, Callable, Dict, List, Optional
911

1012
from scalene.scalene_leak_analysis import ScaleneLeakAnalysis
@@ -13,6 +15,94 @@
1315

1416
import numpy as np
1517

18+
class GPUDevice(str, Enum):
19+
nvidia = "GPU"
20+
neuron = "Neuron"
21+
no_gpu = ""
22+
23+
class FunctionDetail(BaseModel):
24+
line: str
25+
lineno: PositiveInt
26+
memory_samples: List[List[Any]]
27+
n_avg_mb: NonNegativeFloat
28+
n_copy_mb_s: NonNegativeFloat
29+
n_core_utilization: float = Field(confloat(ge=0, le=1))
30+
n_cpu_percent_c: float = Field(confloat(ge=0, le=100))
31+
n_cpu_percent_python: float = Field(confloat(ge=0, le=100))
32+
n_gpu_avg_memory_mb: NonNegativeFloat
33+
n_gpu_peak_memory_mb: NonNegativeFloat
34+
n_gpu_percent: float = Field(confloat(ge=0, le=100))
35+
n_growth_mb: NonNegativeFloat
36+
n_peak_mb: NonNegativeFloat
37+
n_malloc_mb: NonNegativeFloat
38+
n_mallocs: NonNegativeInt
39+
n_python_fraction: float = Field(confloat(ge=0, le=1))
40+
n_sys_percent: float = Field(confloat(ge=0, le=100))
41+
n_usage_fraction: float = Field(confloat(ge=0, le=1))
42+
43+
@model_validator(mode="after")
44+
def check_cpu_percentages(cls, values):
45+
total_cpu_usage = (
46+
values.n_cpu_percent_c
47+
+ values.n_cpu_percent_python
48+
+ values.n_sys_percent
49+
)
50+
if not (total_cpu_usage != 100):
51+
raise ValueError(
52+
"The sum of n_cpu_percent_c, n_cpu_percent_python, and n_sys_percent must be 100"
53+
)
54+
return values
55+
56+
57+
@model_validator(mode="after")
58+
def check_gpu_memory(cls, values):
59+
if values.n_gpu_avg_memory_mb > values.n_gpu_peak_memory_mb:
60+
raise ValueError(
61+
"n_gpu_avg_memory_mb must be less than or equal to n_gpu_peak_memory_mb"
62+
)
63+
return values
64+
65+
@model_validator(mode="after")
66+
def check_cpu_memory(cls, values):
67+
if values.n_avg_mb > values.n_peak_mb:
68+
raise ValueError(
69+
"n_avg_mb must be less than or equal to n_peak_mb"
70+
)
71+
return values
72+
73+
class LineDetail(FunctionDetail):
74+
start_outermost_loop: PositiveInt
75+
end_outermost_loop: PositiveInt
76+
start_region_line: PositiveInt
77+
end_region_line: PositiveInt
78+
79+
80+
class FileDetail(BaseModel):
81+
functions: List[FunctionDetail]
82+
imports: List[str]
83+
leaks: Dict[str, NonNegativeFloat]
84+
lines: List[LineDetail]
85+
percent_cpu_time: NonNegativeFloat
86+
87+
class ScaleneJSONSchema(BaseModel):
88+
alloc_samples: NonNegativeInt
89+
args: List[str]
90+
elapsed_time_sec: NonNegativeFloat
91+
entrypoint_dir: str
92+
filename: str
93+
files: Dict[str, FileDetail]
94+
gpu: StrictBool
95+
gpu_device: GPUDevice
96+
growth_rate: float
97+
max_footprint_fname: Optional[str]
98+
max_footprint_lineno: Optional[PositiveInt]
99+
max_footprint_mb: NonNegativeFloat
100+
max_footprint_python_fraction: NonNegativeFloat
101+
memory: StrictBool
102+
program: str
103+
samples: List[List[NonNegativeFloat]]
104+
stacks: List[List[Any]]
105+
16106

17107
class ScaleneJSON:
18108
@staticmethod
@@ -260,7 +350,7 @@ def output_profile_line(
260350
)
261351
)
262352

263-
return {
353+
payload = {
264354
"lineno": line_no,
265355
"line": line,
266356
"n_core_utilization": mean_core_util,
@@ -280,6 +370,12 @@ def output_profile_line(
280370
"n_copy_mb_s": n_copy_mb_s,
281371
"memory_samples": stats.per_line_footprint_samples[fname][line_no],
282372
}
373+
try:
374+
FunctionDetail(**payload)
375+
except ValidationError as e:
376+
print("Warning: JSON failed validation:")
377+
print(e)
378+
return payload
283379

284380
def output_profiles(
285381
self,
@@ -520,6 +616,13 @@ def output_profiles(
520616
0
521617
]
522618
profile_line["end_outermost_loop"] = outer_loop[lineno][1]
619+
620+
try:
621+
LineDetail(**profile_line)
622+
except ValidationError as e:
623+
print("Warning: JSON failed validation:")
624+
print(e)
625+
523626
# When reduced-profile set, only output if the payload for the line is non-zero.
524627
if reduced_profile:
525628
profile_line_copy = copy.copy(profile_line)
@@ -567,4 +670,10 @@ def output_profiles(
567670
profile_line
568671
)
569672

673+
# Validate the schema
674+
try:
675+
ScaleneJSONSchema(**output)
676+
except ValidationError as e:
677+
print("Warning: JSON failed validation:")
678+
print(e)
570679
return output

0 commit comments

Comments
 (0)