11#!/usr/bin/env ruby
22# coding: utf-8
3+ # =============================================================================
4+ # ze_validator -- a correctness checker for Intel Level Zero ("ze") programs.
5+ #
6+ # WHAT THIS IS
7+ # ------------
8+ # THAPI (Tracing Heterogeneous APIs) ships a tracer that intercepts every call
9+ # an application makes into the Level Zero runtime and writes it to an LTTng
10+ # trace on disk (a "CTF" trace). This script reads such a trace back and
11+ # replays it against a software model of what the Level Zero runtime would have
12+ # been doing, looking for API misuse the runtime itself does not diagnose:
13+ # leaks, use-after-free, deadlocks, cross-context handle mixing, and so on.
14+ #
15+ # It never runs on the GPU and never touches the application. It is a pure
16+ # post-mortem analysis of a recorded trace.
17+ #
18+ # THE FIVE FILES THAT MAKE UP THE VALIDATOR
19+ # -----------------------------------------
20+ # ze_validator.in <-- YOU ARE HERE
21+ # Executable entry point. Parses CLI options, wires up the babeltrace2
22+ # graph that decodes the trace, and hands each decoded event to the
23+ # StateObject. ".in" means autoconf preprocesses it at build time,
24+ # substituting @prefix@ below; the installed file is named `ze_validator`.
25+ #
26+ # ze_validator_zemodel.rb
27+ # Pure data model. Plain Ruby classes mirroring Level Zero objects
28+ # (Device, Context, CommandList, Event, Fence, Memory, ...) plus the
29+ # bookkeeping types used for deferred execution (RecordedOp, DeferredUnit).
30+ # No checking logic lives here -- just state.
31+ #
32+ # ze_validator_state_object.rb
33+ # The engine. Holds the whole model (per host / per process / per thread),
34+ # drives the trace-consumption loop, runs the deferred-execution
35+ # scheduler, and owns every error-reporting method (print_usage_error etc).
36+ #
37+ # ze_validator_function_entry_exit_callbacks.rb
38+ # The dispatch tables. Three global hashes mapping a ze API name to a
39+ # lambda: $upon_entry, $on_successful_exit, $on_erroneous_exit. This is
40+ # where "what does zeMemAllocDevice do to the model" is written.
41+ #
42+ # ze_validator_entry_exit_helpers.rb
43+ # The check library. The `check_*` functions those callbacks call --
44+ # out-of-bounds copies, use-after-free, deadlock cycles, context matching.
45+ #
46+ # HOW A TRACE BECOMES A DIAGNOSTIC (the data flow)
47+ # ------------------------------------------------
48+ # trace on disk
49+ # -> babeltrace2 graph (built in build_and_run_graph below)
50+ # -> StateObject#consume receives one CTF event at a time
51+ # -> event name is matched against /:(z.*)_(entry|exit)/ so
52+ # "lttng_ust_ze:zeMemAllocDevice_entry" yields api="zeMemAllocDevice",
53+ # phase="entry"
54+ # -> StateObject#on_entry / #on_exit look the API up in the dispatch tables
55+ # -> the callback mutates the model and/or calls a check_* helper
56+ # -> a violated check prints "Level Zero <Kind> Error: ..." to stderr
57+ # -> at end of trace StateObject#check_issues reports leaks and deadlocks
58+ #
59+ # WHY EVERY API APPEARS TWICE (entry and exit)
60+ # --------------------------------------------
61+ # The tracer emits one event just BEFORE the call enters the driver (_entry,
62+ # carrying the input arguments) and one just AFTER it returns (_exit, carrying
63+ # the return code and any output pointers). This matters constantly in the
64+ # callback code:
65+ # * Output handles (the thing a Create call produced) only exist at _exit.
66+ # * If the driver CRASHES inside the call, the _exit event is never written --
67+ # so any check that could be the thing that crashes must run at _entry, or
68+ # it will silently never fire. Several callbacks carry a comment saying
69+ # exactly this.
70+ #
71+ # TYPICAL USE
72+ # -----------
73+ # iprof -t -- ./my_app # record a trace with the THAPI tracer
74+ # ze_validator ~/lttng-traces/... # replay it through this validator
75+ # =============================================================================
76+
77+ # Where the installed data files live (ze_thread_safety.yaml, ze_deprecated.json,
78+ # ze_device_property.json) and where the ze_library Ruby bindings are found.
79+ # @prefix@ is replaced by autoconf with the configure --prefix at build time.
380DATADIR = File . join ( "@prefix@" , "share" )
481BINDIR = File . join ( "@prefix@" , "bin" )
582$:. unshift ( DATADIR ) if File . directory? ( DATADIR )
683require 'optparse'
7- require 'babeltrace2'
84+ require 'babeltrace2' # Ruby bindings for babeltrace2, the CTF trace reader
885require 'find'
9- require 'ze_library'
86+ require 'ze_library' # generated FFI bindings: ZE::ZEResult, ZE::ZE*Desc structs
1087require 'pp'
1188require 'set'
12- require 'ze_validator_zemodel'
13- require 'ze_validator_function_entry_exit_callbacks'
14- require 'ze_validator_state_object'
89+ require 'ze_validator_zemodel' # the object model (ZEModel::*)
90+ require 'ze_validator_function_entry_exit_callbacks' # populates $upon_entry / $on_*_exit
91+ require 'ze_validator_state_object' # the StateObject engine
1592require 'yaml'
1693
1794# Don't complain about broken pipe
95+ # (restores the default kill-on-SIGPIPE so piping our output into e.g. `head`
96+ # terminates quietly instead of raising Errno::EPIPE out of a puts)
1897Signal . trap ( 'SIGPIPE' , 'SYSTEM_DEFAULT' )
1998
2099# Runs the ze_device_property helper binary to (re)generate ze_device_property.json
@@ -36,6 +115,12 @@ rescue SystemCallError => e
36115 "continuing without device properties."
37116end
38117
118+ # Command-line defaults. All checking categories are ON unless explicitly
119+ # disabled, so a plain `ze_validator <trace>` gives the most thorough report.
120+ # live - read a live LTTng session instead of a trace directory
121+ # device_agnostic - report portability hazards (hardcoded ordinals etc.)
122+ # performance - report API usage that costs performance
123+ # gen_device_properties - shell out to the ze_device_property helper first
39124$options = { live : false , device_agnostic : true , performance : true ,
40125 gen_device_properties : true }
41126
@@ -66,15 +151,46 @@ OptionParser.new do |opts|
66151end . parse!
67152
68153
154+ # Builds and runs the babeltrace2 processing graph that decodes the trace and
155+ # feeds it to the validator, then triggers the end-of-trace reporting pass.
156+ #
157+ # babeltrace2 works as a dataflow graph of components connected port-to-port:
158+ #
159+ # [source: trace_0] --\
160+ # [source: trace_1] ----> [filter: muxer] --> [sink: our StateObject]
161+ # [source: trace_2] --/
162+ #
163+ # * SOURCES decode one CTF trace directory each into a stream of messages.
164+ # A traced run produces one trace per process (and per node on a cluster),
165+ # hence potentially many sources.
166+ # * The MUXER merges those streams into a single stream ordered by timestamp.
167+ # This is essential: the validator's model assumes it sees events in the
168+ # order they really happened, across all processes and threads.
169+ # * The SINK is us -- sink_object.consume is a lambda invoked with each batch
170+ # of messages (see StateObject#consume).
171+ #
172+ # `source_location` is the list of paths given on the command line.
69173def build_and_run_graph ( source_location , sink_object )
70174 # build graph and set up source
71175 graph = BT2 ::BTGraph . new
72176
177+ # Look up the three component classes we need from babeltrace2's plugins.
178+ # ctf.fs - read a CTF trace from the filesystem
179+ # ctf.lttng-live - attach to a running LTTng session over the network
180+ # utils.muxer - timestamp-order-merge several streams into one
73181 ctf_fs = BT2 ::BTPlugin . find ( 'ctf' ) . get_source_component_class_by_name ( 'fs' )
74182 ctf_lttng_live = BT2 ::BTPlugin . find ( "ctf" ) . get_source_component_class_by_name ( "lttng-live" )
75183 utils_muxer = BT2 ::BTPlugin . find ( 'utils' ) . get_filter_component_class_by_name ( 'muxer' )
76184
77185 if !$options[ :live ]
186+ # Offline mode: the user hands us a directory that may contain many traces
187+ # nested at arbitrary depth (LTTng lays out one subdirectory per process,
188+ # per node, per UST channel). Walk it and work out which directories are
189+ # actually readable CTF traces, in four steps:
190+ # 1. recursively enumerate every path, keeping only files
191+ # 2. keep the ones literally named "metadata" -- every CTF trace directory
192+ # contains exactly one such file describing its event layout
193+ # 3. take that file's parent directory: that IS the trace directory
78194 trace_locations =
79195 Find . find ( *source_location ) . reject do |path |
80196 FileTest . directory? ( path )
@@ -88,10 +204,16 @@ def build_and_run_graph( source_location, sink_object )
88204 qe . query . value [ 'weight' ] > 0.5
89205 end
90206 else
207+ # Live mode: the arguments are LTTng relay-daemon URLs, not paths, so there
208+ # is nothing on disk to search -- pass them through untouched.
91209 trace_locations = source_location
92210 end
93211 raise 'Could not find lttng trace' if trace_locations . size == 0
94212
213+ # Add one source component per trace. Each gets a unique name ("trace_0",
214+ # "trace_1", ...) because babeltrace requires component names to be distinct.
215+ # In live mode, "session-not-found-action" => "end" makes the component finish
216+ # cleanly rather than hang forever when the named session does not exist.
95217 if !$options[ :live ]
96218 comp_sources = trace_locations . each_with_index . collect { |trace_location , i | graph . add_component ( ctf_fs , "trace_#{ i } " , params : { "inputs" => [ trace_location ] } ) }
97219 else
@@ -101,32 +223,59 @@ def build_and_run_graph( source_location, sink_object )
101223 # Muxer
102224 comp_muxer = graph . add_component ( utils_muxer , 'mux' )
103225
226+ # The sink is where our code plugs in: `sink_object.consume` returns a lambda
227+ # that babeltrace calls with an iterator over each batch of decoded messages.
228+ # That lambda is the top of the whole validation pipeline.
104229 sink = graph . add_simple_sink ( 'babeltrace_thapi' , sink_object . consume )
105230
106231 # Sources to muxer
232+ # A single trace may expose several output ports (one per CTF stream, i.e.
233+ # roughly per traced thread), so flat_map over all sources' ports and wire
234+ # port i of the collected list into muxer input port i. The muxer grows its
235+ # input ports on demand, which is why we can index it freely.
107236 comp_sources . flat_map ( &:output_ports ) . each_with_index do |op , i |
108237 ip = comp_muxer . input_port ( i )
109238 graph . connect_ports ( op , ip )
110239 end
111240
112241 # Chain the rest
242+ # Only one link is left (muxer -> sink). It is written as a generic
243+ # each_cons(2) pairwise chain so extra filter components could be spliced into
244+ # the array without changing this code.
113245 [ comp_muxer , sink ] . flatten . each_cons ( 2 ) do |_out , _in |
114246 op = _out . output_port ( 0 )
115247 ip = _in . input_port ( 0 )
116248 graph . connect_ports ( op , ip )
117249 end
118250
251+ # Pull the whole trace through the graph. This call does not return until
252+ # every event has been consumed, so by the time it finishes the model
253+ # reflects the entire run.
119254 graph . run
255+ # End-of-trace pass: drain any still-pending deferred command lists, report
256+ # deadlocks, and report every object that was created but never destroyed.
257+ # Errors found DURING the run were already printed as they were discovered;
258+ # these are the ones only visible once you know nothing more is coming.
120259 sink_object . check_issues ( )
121260
122261end
123262
124263# only executive this code if we launch this as the main
125264# script. if it's just included with "require" we just want access to the functions.
265+ # (Ruby's equivalent of C's `int main` guard: $0 is the script that was invoked,
266+ # __FILE__ is this file, so they match only when run directly. Tests can then
267+ # `require` this file to reuse build_and_run_graph without starting an analysis.)
126268if __FILE__ == $0
269+ # Refresh ze_device_property.json first, so StateObject picks up the real
270+ # command-queue-group topology of this machine when it is constructed below.
127271 generate_device_properties if $options[ :gen_device_properties ]
272+ # One StateObject holds the entire model for the whole run and acts as the
273+ # graph's sink.
128274 sink_obj = StateObject . new ( device_agnostic : $options[ :device_agnostic ] ,
129275 performance : $options[ :performance ] )
276+ # Whatever is left in ARGV after OptionParser#parse! are the trace paths.
277+ # Deduplicate: feeding the same trace twice would double-apply every event and
278+ # corrupt the model (e.g. a second create for an already-live handle).
130279 ARGV . uniq!
131280 source_location = ARGV
132281 build_and_run_graph ( source_location , sink_obj )
0 commit comments