3
3
import re
4
4
5
5
from collections import OrderedDict , defaultdict
6
+ from enum import Enum
6
7
from operator import itemgetter
7
8
from pathlib import Path
9
+ from pydantic import BaseModel , Field , NonNegativeFloat , NonNegativeInt , PositiveInt , StrictBool , ValidationError , confloat , model_validator
8
10
from typing import Any , Callable , Dict , List , Optional
9
11
10
12
from scalene .scalene_leak_analysis import ScaleneLeakAnalysis
13
15
14
16
import numpy as np
15
17
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
+
16
106
17
107
class ScaleneJSON :
18
108
@staticmethod
@@ -260,7 +350,7 @@ def output_profile_line(
260
350
)
261
351
)
262
352
263
- return {
353
+ payload = {
264
354
"lineno" : line_no ,
265
355
"line" : line ,
266
356
"n_core_utilization" : mean_core_util ,
@@ -280,6 +370,12 @@ def output_profile_line(
280
370
"n_copy_mb_s" : n_copy_mb_s ,
281
371
"memory_samples" : stats .per_line_footprint_samples [fname ][line_no ],
282
372
}
373
+ try :
374
+ FunctionDetail (** payload )
375
+ except ValidationError as e :
376
+ print ("Warning: JSON failed validation:" )
377
+ print (e )
378
+ return payload
283
379
284
380
def output_profiles (
285
381
self ,
@@ -520,6 +616,13 @@ def output_profiles(
520
616
0
521
617
]
522
618
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
+
523
626
# When reduced-profile set, only output if the payload for the line is non-zero.
524
627
if reduced_profile :
525
628
profile_line_copy = copy .copy (profile_line )
@@ -567,4 +670,10 @@ def output_profiles(
567
670
profile_line
568
671
)
569
672
673
+ # Validate the schema
674
+ try :
675
+ ScaleneJSONSchema (** output )
676
+ except ValidationError as e :
677
+ print ("Warning: JSON failed validation:" )
678
+ print (e )
570
679
return output
0 commit comments