#!/usr/bin/env perl use strict; use warnings; use diagnostics; use FindBin; use lib $FindBin::Bin; use Util; use Mars; use TUI; use Text::CSV_XS; use File::Temp qw(tempfile); use feature 'say'; my $local_wamr = '/home/christoph/Notes/TU/MastersThesis/05 WAMR'; my $local_newlib = '/home/christoph/Notes/TU/MastersThesis/07 NewLib'; my $local_root = '/home/christoph/Notes/TU/MastersThesis/FailNix'; my $local_archive_dir = "$local_root/injections"; # Select experiment to open my @experiments = Util::find_subdirs($local_archive_dir); my @selected_experiments = TUI::select_from_list( "Select Archived Experiment to Open", 0, @experiments ); die "No experiment selected" unless @selected_experiments; my $selected_experiment = $selected_experiments[0]; my $cui = TUI::init_cui(); # TODO: Add a TextEditor panel beside the experiment selection for notes. # Store notes in experiment/notes/description.txt # TODO: Add a TextEditor panel below the markers for notes. # Store notes in experiment/notes/benchmark-type-address.txt # TODO: Add a more exhaustive faults query that also retrieves # what fault has been injected. Then add an info popup or # display it in the markers panel # TODO: Add a filter for address ranges # TODO: Add a filter for segments # ============================================================================= # Data handling # ============================================================================= my @marker_values; my %marker_labels; my @filtered_marker_values; my $assembly_text; my $source_file; my $source_line; my $source_text; my @benchs = ( "ip", "mem", "regs" ); my @markers = ( "OK_MARKER", "DETECTED_MARKER", "FAIL_MARKER", "TRAP", "TIMEOUT", "ACCESS_OUTERSPACE", "WRITE_TEXTSEGMENT", ); # Filter popup state my $popup_focus = "left"; my @selected_benchs = @benchs; my %index_of_benchs; @index_of_benchs{@benchs} = 0 .. $#benchs; my @selected_markers = ("FAIL_MARKER"); my %index_of_markers; @index_of_markers{@markers} = 0 .. $#markers; my $selected_threshold = 0; sub load_faults_csv { @marker_values = (); %marker_labels = (); # Schema: benchmark, resulttype, faults, fault_address my $data = Text::CSV_XS::csv( in => "$local_archive_dir/$selected_experiment/faults.csv", headers => 'auto' ); my @sections = Util::elf_read_sections( "$local_archive_dir/$selected_experiment/system.elf"); # Result: # [ { benchmark => "ip", resulttype => "OK_MARKER", # faults => "100", fault_address => "0x100074" }, # ... ] @marker_values = @$data; @filtered_marker_values = @marker_values; %marker_labels = map { $_ => sprintf( "%5s %10s %10s %20s %15sx", $_->{benchmark}, $_->{fault_address}, Util::get_section_name( $_->{fault_address}, @sections ), $_->{resulttype}, Util::format_number_sep( $_->{faults} ), ); } @marker_values; } sub filter_marker_values { sub is_allowed { my ($marker) = @_; my $bench = $marker->{benchmark}; my $type = $marker->{resulttype}; my $count = int( $marker->{faults} ); return ( grep { /^$bench$/ } @selected_benchs and grep { /^$type$/ } @selected_markers and $count > $selected_threshold ); } @filtered_marker_values = grep { is_allowed($_) } @marker_values; } # How much to disassemble around the address / how many lines to show my $region_size = $cui->height() / 2 - 2; my $asm_region_size = $region_size + 120; my %assembly_cache; sub align_region { my ( $assembly_region, $objdump_address ) = @_; my @region; my @lines = split "\n", $assembly_region; my $center; for my $i ( 1 .. $#lines ) { my $line = $lines[ $i - 1 ]; chomp $line; # Mark the center line if ( grep { /^\s*$objdump_address/ } $line ) { push @region, sprintf( ">> %s", $line ); $center = $i; } else { push @region, sprintf( " %s", $line ); } } if ( defined $center ) { my $offset = $center - $region_size; if ( $offset > 0 ) { # Remove lines ($center > $region_size) @region = @region[ $offset .. $#region ]; } elsif ( $offset < 0 ) { # Add lines for ( 1 .. abs($offset) + 1 ) { unshift @region, ""; } } } return join "\n", @region; } sub load_assembly_elf { my ($marker) = @_; return unless defined $marker; my $address = hex( $marker->{fault_address} ); if ( !exists $assembly_cache{$address} ) { my $file = "$local_archive_dir/$selected_experiment/system.elf"; my $start = $address - $asm_region_size; my $end = $address + $asm_region_size; my $objdump_address = lc substr( $marker->{fault_address}, 2 ); my $assembly_region = qx{objdump $file -D -M intel -C --start-address=$start --stop-address=$end} =~ s/.*\n.*\n.*\n.*\n.*\n.*\n//r; $assembly_cache{$address} = align_region( $assembly_region, $objdump_address ); } $assembly_text = $assembly_cache{$address}; } my %remappings = ( "/build/source/core" => "$local_wamr/core", "/build/newlib-.*/newlib" => "$local_newlib/newlib", ".*/module_host.c" => "$local_archive_dir/$selected_experiment/module_host.c", ); sub remap_source_path { my ($path) = @_; foreach my $source ( keys %remappings ) { my $target = $remappings{$source}; $path =~ s/$source/$target/; } return $path; } sub read_source_file { my ( $sourcefile, $lineno ) = @_; return unless ( -f $sourcefile ); open( my $fhandle, '<', $sourcefile ) or return; my @lines = <$fhandle>; close($fhandle); my $start = int($lineno) - $region_size; $start = 1 if $start < 1; my $end = int($lineno) + $region_size; $end = scalar(@lines) if $end > scalar(@lines); my @region; for my $i ( $start .. $end ) { my $line = $lines[ $i - 1 ]; chomp $line; # Mark the center line if ( $i == $lineno ) { push @region, sprintf( ">> %6d | %s", $i, $line ); } else { push @region, sprintf( " %6d | %s", $i, $line ); } } return join( "\n", @region ); } my %source_cache; my %source_file_cache; my %source_line_cache; sub load_source_c { my ($marker) = @_; return unless defined $marker; my $address = $marker->{fault_address}; if ( !exists $source_cache{$address} ) { my $elffile = "$local_archive_dir/$selected_experiment/system.elf"; my $host_file = "$local_archive_dir/$selected_experiment/module_host.c"; my $module_file = "$local_archive_dir/$selected_experiment/wasm-module.cpp"; # Example: # wasm_interp_call_func_bytecode # /build/source/core/iwasm/interpreter/wasm_interp_classic.c:1685 my $location = qx{addr2line -e $elffile -f -C -i $address}; my @lines = split( "\n", $location ); if ( scalar(@lines) < 2 ) { $source_text = "Addr2Line Error\n"; return; } my $line = $lines[1]; # Ignore possible further lines my ( $sourcefile, $lineno ) = split ":", $line; ($lineno) = $lineno =~ /^(\d+)/; my $remapped = remap_source_path($sourcefile); my $source_region = read_source_file( $remapped, $lineno ); if ( !defined $source_region or length($source_region) == 0 ) { $source_text = "Failed to Read Source: $location"; $source_file = "Unknown"; $source_line = "Unknown"; return; } $source_cache{$address} = $source_region; $source_file_cache{$address} = $remapped; $source_line_cache{$address} = $lineno; } $source_text = $source_cache{$address}; $source_file = $source_file_cache{$address}; $source_line = $source_line_cache{$address}; } # ============================================================================= # UI Layout # ============================================================================= my $win = $cui->add( 'explorer', 'Window' ); my $left = $win->add( 'left', 'Container' ); # , -padbottom => 1, my $middle = $win->add( 'middle', 'Container' ); # , -padbottom => 1, my $right = $win->add( 'right', 'Container' ); # , -padbottom => 1, my $markers_panel; my $assembly_panel; my $source_panel; my $refresh_panels = sub { load_assembly_elf( $markers_panel->get_active_value() ); load_source_c( $markers_panel->get_active_value() ); $assembly_panel->text($assembly_text); $source_panel->text($source_text); $source_panel->title("Source View: $source_file"); $assembly_panel->draw(); $source_panel->draw(); }; $markers_panel = $left->add( 'markers_list', 'Listbox', -title => "Markers", -border => 1, -values => \@filtered_marker_values, -labels => \%marker_labels, -multi => 0, -radio => 0, -vscrollbar => 'right', -onselchange => $refresh_panels, ); $assembly_panel = $middle->add( 'assembly_viewer', 'TextViewer', -title => "Assembly View", -border => 1, -text => $assembly_text, ); $source_panel = $right->add( 'source_viewer', 'TextViewer', -title => "Source View", -border => 1, -text => $source_text, ); $win->add( 'titlebar', 'Label', -y => 0, -text => "Experiment: $selected_experiment", ); $win->add( 'info', 'Label', -y => -1, -text => "Space = toggle, F = filter markers, S = open source, A = open assembly, R = resize, Q = quit", ); sub set_geometry { my ( $widget, $x, $y, $w, $h ) = @_; $widget->{-x} = $x; $widget->{-y} = $y; $widget->{-width} = $w; $widget->{-height} = $h; # $widget->layout(); } sub layout_resize { my $w = $win->width(); my $h = $win->height(); my $w1 = int( $w * 0.3 ); my $w2 = int( $w * 0.3 ); my $w3 = int( $w - $w1 - $w2 ); set_geometry( $left, 0, 1, $w1, $h - 2 ); set_geometry( $middle, $w1, 1, $w2, $h - 2 ); set_geometry( $right, $w1 + $w2, 1, $w3, $h - 2 ); $cui->layout(); } # ============================================================================= # Filter Popup # ============================================================================= sub markers_filter_popup { my $w = 60; my $h = 24; my $h1 = $h - 7; # top listboxes height my @bench_values = @benchs; my %bench_labels = map { $_ => $_ } @benchs; my @marker_values = @markers; my %marker_labels = map { $_ => $_ } @markers; my $popup = $cui->add( 'popup', 'Window', -x => int( $cui->width() / 2 - $w / 2 ), -y => int( $cui->height() / 2 - $h / 2 ), -width => $w, -height => $h, -border => 1, -title => "Filter Markers", ); my $popup_left = $popup->add( 'popup_left', 'Container', -x => 0, -y => 1, -width => $w / 2, -height => $h1, ); my $popup_right = $popup->add( 'popup_right', 'Container', -x => $w / 2, -y => 1, -width => $w / 2, -height => $h1, ); my $popup_bottom = $popup->add( 'popup_bottom', 'Container', -x => 0, -y => $h1 + 1, -width => $w, -height => 3, ); my $benchmark_filter = $popup_left->add( 'benchmark_filter', 'Listbox', -title => "By Benchmark", -border => 1, -values => \@bench_values, -labels => \%bench_labels, -multi => 1, -radio => 0, ); my $marker_filter = $popup_right->add( 'marker_filter', 'Listbox', -title => "By Type", -border => 1, -values => \@marker_values, -labels => \%marker_labels, -multi => 1, -radio => 0, ); my $threshold = $popup_bottom->add( 'threshold', 'TextEntry', -title => "By Count Threshold", -border => 1, -text => $selected_threshold, -regexp => '/^\d*$/', ); $popup->add( 'popup_info', 'Label', -y => -1, -text => "Space = toggle, F = cycle focus, C = confirm, Q = quit", ); $benchmark_filter->set_selection( map { $index_of_benchs{$_} } @selected_benchs ); $marker_filter->set_selection( map { $index_of_markers{$_} } @selected_markers ); my $focus = sub { if ( $popup_focus eq "left" ) { $benchmark_filter->focus(); } elsif ( $popup_focus eq "right" ) { $marker_filter->focus(); } else { $threshold->focus(); } }; $popup->set_binding( sub { @selected_benchs = $benchmark_filter->get(); @selected_markers = $marker_filter->get(); $selected_threshold = int( $threshold->get() ) or 0; filter_marker_values( @selected_benchs, @selected_markers ); $cui->delete('popup'); $cui->layout(); }, 'c', ); $popup->set_binding( sub { if ( $popup_focus eq "left" ) { $popup_focus = "right"; } elsif ( $popup_focus eq "right" ) { $popup_focus = "bottom"; } else { $popup_focus = "left"; } $focus->(); }, 'f', ); $popup->set_binding( sub { $cui->delete('popup'); $cui->layout(); }, 'q' ); $cui->layout(); $focus->(); } # ============================================================================= # Bindings # ============================================================================= $win->set_binding( sub { markers_filter_popup(); }, 'f', ); $win->set_binding( sub { layout_resize(); }, 'r', ); $win->set_binding( sub { system( 'neovide', '--fork', $source_file, '--', "+$source_line" ); }, 's', ); my $objdump_out; my @objdump_lines; my $temp_file; $win->set_binding( sub { my $file = "$local_archive_dir/$selected_experiment/system.elf"; my $marker = $markers_panel->get_active_value(); my $objdump_address = lc substr( $marker->{fault_address}, 2 ); if ( !defined $objdump_out ) { $objdump_out = qx{objdump $file -D -M intel -C --source}; $objdump_out =~ s/.*\n.*\n.*\n.*\n.*\n.*\n//; @objdump_lines = split "\n", $objdump_out; ( my $fhandle, $temp_file ) = tempfile(); print $fhandle $objdump_out; close($fhandle); } my $temp_line; for my $i ( 1 .. scalar(@objdump_lines) ) { my $line = $objdump_lines[ $i - 1 ]; if ( grep { /^\s*$objdump_address/ } $line ) { $temp_line = $i; last; } } if ( defined $temp_line ) { system( 'neovide', '--fork', $temp_file, '--', "+$temp_line" ); } else { system( 'neovide', '--fork', $temp_file ); } }, 'a', ); $win->set_binding( sub { }, 'g', ); $win->set_binding( sub { $cui->mainloopExit(); }, 'q', ); # ============================================================================= # Main Loop # ============================================================================= load_faults_csv(); filter_marker_values(); layout_resize(); $markers_panel->focus(); $refresh_panels->(); $cui->mainloop(); $cui->leave_curses(); $cui->delete('explorer');