22
33# MIT License
44#
5- # Copyright (c) 2024-2025 Advanced Micro Devices,
6- # Inc. All rights reserved.
5+ # Copyright (c) 2024-2025 Advanced Micro Devices, Inc.
76#
87# Permission is hereby granted, free of charge, to any person obtaining a copy
98# of this software and associated documentation files (the "Software"), to deal
2322# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2423# THE SOFTWARE.
2524
25+
2626import os
2727import sys
2828import csv
29- import json
3029import subprocess
3130import pytest
3231
33- def node_exists (name , data , min_len = 1 ):
34- assert name in data , f"missing key: { name } "
35- assert data [name ] is not None , f"key is None: { name } "
36- if hasattr (data [name ], "__len__" ):
37- assert len (data [name ]) >= min_len , f"key '{ name } ' too small"
3832
3933def run_rocpd_convert (db_path , out_dir ):
34+ """Convert rocpd database to CSV format."""
4035 os .makedirs (out_dir , exist_ok = True )
4136 cmd = [sys .executable , "-m" , "rocpd" , "convert" , "-i" , db_path , "--output-format" , "csv" , "-d" , out_dir ]
4237 res = subprocess .run (cmd , stdout = subprocess .PIPE , stderr = subprocess .PIPE , text = True )
4338 assert res .returncode == 0 , f"rocpd convert failed\n cmd={ ' ' .join (cmd )} \n stdout={ res .stdout } \n stderr={ res .stderr } "
4439
40+
4541def find_kernel_trace_csv (out_dir ):
42+ """Locate kernel_trace CSV file in output directory."""
4643 for fn in os .listdir (out_dir ):
4744 if fn .endswith ("kernel_trace.csv" ):
4845 return os .path .join (out_dir , fn )
4946 assert False , f"kernel trace CSV not found in { out_dir } "
5047
48+
5149def load_csv_rows (path ):
50+ """Load CSV file and return rows as list of dicts."""
5251 assert os .path .isfile (path ), f"missing CSV: { path } "
5352 with open (path , newline = "" ) as f :
5453 reader = csv .DictReader (f )
5554 rows = list (reader )
5655 assert len (rows ) > 0 , f"empty CSV: { path } "
5756 return rows
5857
58+
5959def extract_json_kernel_records (json_root ):
60- node_exists ("rocprofiler-sdk-tool" , json_root )
60+ """Extract kernel dispatch records from JSON output."""
61+ assert "rocprofiler-sdk-tool" in json_root , "missing rocprofiler-sdk-tool in JSON"
6162 tool = json_root ["rocprofiler-sdk-tool" ]
6263 if isinstance (tool , list ) and len (tool ) > 0 :
6364 tool = tool [0 ]
64- node_exists ( "buffer_records" , tool )
65+ assert "buffer_records" in tool , "missing buffer_records in JSON"
6566 br = tool ["buffer_records" ]
67+
6668 for key in ("kernel_dispatch" , "kernel_trace" , "kernel_dispatch_trace" ):
6769 if key in br and isinstance (br [key ], list ) and len (br [key ]) > 0 :
6870 return br [key ]
69- assert False , f"cannot find kernel dispatch records in buffer_records keys={ list (br .keys ())} "
71+ assert False , f"no kernel dispatch records found in JSON buffer_records keys={ list (br .keys ())} "
7072
71- def build_json_map (records ):
73+
74+ def build_json_duration_map (records ):
75+ """Build map of dispatch_id -> (start, end, duration) from JSON records."""
7276 m = {}
7377 for r in records :
78+ # Extract dispatch ID
7479 dispatch_info = r .get ("dispatch_info" , {})
7580 dispatch_id = dispatch_info .get ("dispatch_id" ) if isinstance (dispatch_info , dict ) else None
76- corr_id = r .get ("correlation_id" , {})
77- if isinstance (corr_id , dict ):
78- corr_id = corr_id .get ("internal" , 0 )
81+
82+ # Fallback to correlation_id if no dispatch_id
83+ if dispatch_id is None :
84+ corr_id = r .get ("correlation_id" , {})
85+ if isinstance (corr_id , dict ):
86+ dispatch_id = corr_id .get ("internal" , 0 )
87+ else :
88+ dispatch_id = corr_id
89+
90+ # Extract timestamps
7991 start = r .get ("start_timestamp" )
8092 end = r .get ("end_timestamp" )
81- assert start is not None and end is not None , f"missing start/end in json record: { r } "
93+ assert start is not None and end is not None , f"missing timestamps in JSON record: { r } "
94+
8295 start = int (start )
8396 end = int (end )
84- assert start > 0 and end > 0 , f"non-positive timestamps start={ start } end={ end } "
97+ assert start > 0 and end > 0 , f"invalid timestamps start={ start } end={ end } "
8598 assert end >= start , f"end before start: start={ start } end={ end } "
86- key = dispatch_id if dispatch_id is not None else corr_id
87- assert key is not None , f"no key to match json record: { r } "
88- m [str (key )] = (start , end )
89- assert len (m ) > 0 , "no records found in JSON"
99+
100+ duration = end - start
101+ m [str (dispatch_id )] = (start , end , duration )
102+
103+ assert len (m ) > 0 , "no kernel records extracted from JSON"
90104 return m
91105
106+
92107def test_rocpd_kernel_trace_duration (json_data , db_path , tmp_path ):
108+ """
109+ Test that rocpd CSV output contains Duration column and values match JSON.
110+
111+ Test strategy:
112+ 1. Generate JSON and rocpd output from SAME execution (using ROCPROF_OUTPUT_FORMAT env var)
113+ 2. Use rocpd to convert database to CSV
114+ 3. Compare CSV Duration with JSON-derived duration
115+
116+ Since JSON and rocpd come from the same execution, timestamps should be IDENTICAL.
117+ We expect ZERO tolerance for differences.
118+
119+ Validates:
120+ - Duration column exists in CSV
121+ - Duration values EXACTLY match between JSON and CSV (zero tolerance)
122+ - Duration correctly calculated as End - Start
123+ - Start and End timestamps also match exactly
124+ """
125+ # Convert rocpd DB to CSV
93126 out_dir = tmp_path / "rocpd_csv"
94127 run_rocpd_convert (db_path , str (out_dir ))
95128 csv_path = find_kernel_trace_csv (str (out_dir ))
96- rows = load_csv_rows (csv_path )
129+ csv_rows = load_csv_rows (csv_path )
97130
98- # Main test: verify Duration column exists
99- assert "Duration" in rows [0 ], f"missing 'Duration' column; columns={ list (rows [0 ].keys ())} "
131+ # Verify Duration column exists
132+ assert "Duration" in csv_rows [0 ], f"missing 'Duration' column; columns={ list (csv_rows [0 ].keys ())} "
100133
134+ # Extract JSON data
101135 json_records = extract_json_kernel_records (json_data )
102- json_map = build_json_map (json_records )
136+ json_map = build_json_duration_map (json_records )
103137
104- for row in rows :
105- key = row .get ("Dispatch_Id" ) or row .get ("Correlation_Id" )
106- assert key is not None , f"cannot match row: { row } "
138+ # Track statistics
139+ matched_count = 0
140+ total_count = len (csv_rows )
141+ mismatches = []
142+
143+ for csv_row in csv_rows :
144+ # Get CSV values
145+ csv_start = int (csv_row ["Start_Timestamp" ])
146+ csv_end = int (csv_row ["End_Timestamp" ])
147+ csv_dur = int (csv_row ["Duration" ])
148+
149+ # Validate CSV internal consistency
150+ assert csv_start > 0 and csv_end > 0 , f"invalid CSV timestamps: start={ csv_start } end={ csv_end } "
151+ assert csv_end >= csv_start , f"CSV end before start: { csv_end } < { csv_start } "
152+ assert csv_dur >= 0 , f"negative CSV duration: { csv_dur } "
153+ assert csv_dur == (csv_end - csv_start ), f"CSV duration mismatch: { csv_dur } != { csv_end - csv_start } "
107154
108- start = int (row ["Start_Timestamp" ])
109- end = int (row ["End_Timestamp" ])
110- dur = int (row ["Duration" ])
155+ # Match with JSON and require EXACT match (zero tolerance)
156+ dispatch_id = csv_row .get ("Dispatch_Id" ) or csv_row .get ("Correlation_Id" )
157+ if dispatch_id and str (dispatch_id ) in json_map :
158+ matched_count += 1
159+ json_start , json_end , json_dur = json_map [str (dispatch_id )]
160+
161+ # Check for exact match on all three values
162+ start_diff = csv_start - json_start
163+ end_diff = csv_end - json_end
164+ dur_diff = csv_dur - json_dur
165+
166+ if start_diff != 0 or end_diff != 0 or dur_diff != 0 :
167+ mismatches .append ({
168+ 'dispatch_id' : dispatch_id ,
169+ 'csv_start' : csv_start ,
170+ 'json_start' : json_start ,
171+ 'start_diff' : start_diff ,
172+ 'csv_end' : csv_end ,
173+ 'json_end' : json_end ,
174+ 'end_diff' : end_diff ,
175+ 'csv_dur' : csv_dur ,
176+ 'json_dur' : json_dur ,
177+ 'dur_diff' : dur_diff
178+ })
179+
180+ # Report any mismatches
181+ if mismatches :
182+ error_lines = [
183+ "" ,
184+ "TIMESTAMP MISMATCHES DETECTED" ,
185+ f"{ 'Dispatch' :<10} { 'Start Diff' :<12} { 'End Diff' :<12} { 'Dur Diff' :<12} " ,
186+ "=" * 50
187+ ]
111188
112- # Verify timestamps are reasonable
113- assert start > 0 and end > 0 , f"non-positive timestamps in CSV: start= { start } end= { end } "
114- assert end >= start , f"end before start in CSV: start= { start } end= { end } "
115- assert dur >= 0 , f"negative duration in CSV: dur= { dur } "
189+ for m in mismatches [: 10 ]: # Show first 10
190+ error_lines . append (
191+ f" { m [ 'dispatch_id' ]:<10 } { m [ 'start_diff' ]:<12 } { m [ 'end_diff' ]:<12 } { m [ 'dur_diff' ]:<12 } "
192+ )
116193
117- # Verify Duration column is correctly calculated from Start and End
118- assert dur == ( end - start ), f"duration mismatch in CSV: dur= { dur } vs calculated= { end - start } "
194+ if len ( mismatches ) > 10 :
195+ error_lines . append ( f"... and { len ( mismatches ) - 10 } more mismatches" )
119196
120- if str (key ) in json_map :
121- jstart , jend = json_map [str (key )]
122- json_dur = jend - jstart
123- diff = abs (json_dur - dur )
124- # Allow small differences due to clock skew/adjustments
125- # Just warn if difference is significant (>10% or >1000ns)
126- if diff > 1000 and diff > dur * 0.1 :
127- print (f"INFO: Large timestamp difference for key={ key } : csv_dur={ dur } json_dur={ json_dur } diff={ diff } " )
197+ # Fail the test with detailed error
198+ first = mismatches [0 ]
199+ error_msg = "\n " .join (error_lines ) + "\n \n " + (
200+ f"Timestamp mismatch detected for dispatch { first ['dispatch_id' ]} :\n "
201+ f" CSV: start={ first ['csv_start' ]} , end={ first ['csv_end' ]} , duration={ first ['csv_dur' ]} \n "
202+ f" JSON: start={ first ['json_start' ]} , end={ first ['json_end' ]} , duration={ first ['json_dur' ]} \n "
203+ f" Diff: start={ first ['start_diff' ]} , end={ first ['end_diff' ]} , duration={ first ['dur_diff' ]} \n "
204+ f"Total mismatches: { len (mismatches )} /{ total_count } \n "
205+ f"NOTE: Since JSON and rocpd come from the same execution, timestamps should be identical."
206+ )
207+ assert False , error_msg
208+
209+ # Ensure we matched all records
210+ assert matched_count > 0 , f"No CSV rows matched with JSON records"
211+ assert matched_count == total_count , f"Only { matched_count } /{ total_count } CSV rows matched JSON"
212+
128213
129214if __name__ == "__main__" :
130215 rc = pytest .main (["-x" , __file__ ] + sys .argv [1 :])
131- sys .exit (rc )
216+ sys .exit (rc )
217+
0 commit comments