package Kratos::DFADriver; use strict; use warnings; use 5.020; use parent 'Class::Accessor'; use Archive::Tar; use AspectC::Repo; use Carp; use Carp::Assert::More; use Cwd; use Data::Dumper; use DateTime; use Device::SerialPort; use File::Slurp qw(read_dir read_file write_file); use IPC::Run qw(harness); use JSON; use Kratos::DFADriver::DFA; use Kratos::DFADriver::Model; use List::Util qw(first); use List::MoreUtils qw(pairwise); use MIMOSA; use MIMOSA::Log; Kratos::DFADriver->mk_ro_accessors(qw(class_name dfa mimosa model repo)); our $VERSION = '0.00'; sub new { my ( $class, %opt ) = @_; my $self = \%opt; $self->{dfa} = Kratos::DFADriver::DFA->new(%opt); $self->{mimosa} = MIMOSA->new(%opt); $self->{repo} = AspectC::Repo->new; $self->{lp}{iteration} = 1; if ( -r $opt{model_file} ) { $self->{model} = Kratos::DFADriver::Model->new(%opt); $self->{class_name} = $self->{model}->class_name; } elsif ( $opt{class_name} ) { $self->{model} = Kratos::DFADriver::Model->new_from_repo( repo => $self->{repo}, class_name => $opt{class_name}, model_file => $opt{model_file}, ); } else { die('Neither driver.json nor class name specified, cannot continue'); } bless( $self, $class ); $self->set_paths; $self->dfa->set_model( $self->model ); return $self; } sub set_paths { my ($self) = @_; my $model_path = $self->{model_file}; $model_path =~ s{ /?+dfa-driver/[^/]+[.] ( xml | json ) $ }{}x; my $prefix = $self->{prefix} = cwd() . "/${model_path}/src"; my $class_prefix = $self->repo->get_class_path_prefix( $self->{class_name} ); $self->{ah_file} = "${prefix}/${class_prefix}_dfa.ah"; $self->{cc_file} = "${prefix}/${class_prefix}_dfa.cc.inc"; $self->{h_file} = "${prefix}/${class_prefix}_dfa.h.inc"; } sub set_output { my ( $self, $mode ) = @_; if ( $mode eq 'tex' ) { $self->{tex} = 1; } return $self; } sub preprocess { my ( $self, @files ) = @_; my @logs; my @json_files; for my $i ( 0 .. $#files ) { push( @logs, MIMOSA::Log->new( data_file => $files[$i], fast_analysis => $self->{fast_analysis}, model => $self->model, merge_args => $self->{merge_args}, tmpsuffix => $i, ) ); } for my $log (@logs) { if ( not $self->{cache} or not $log->load_cache ) { $log->load_archive; $log->preprocess; $log->save_cache; } push( @json_files, $log->json_name ); } $self->{log} = $logs[0]; return ( \@logs, \@json_files ); } sub analyze { my ( $self, @files ) = @_; my ( $logs, $json_files ) = $self->preprocess(@files); $self->log->analyze( @{$json_files} ); } sub crossvalidate_model { my ( $self, @files ) = @_; my ( $logs, $json_files ) = $self->preprocess(@files); $self->log->crossvalidate( @{$json_files} ); } sub log { my ( $self, $file ) = @_; if ($file) { $self->{log} = undef; } $self->{log} //= MIMOSA::Log->new( data_file => $file // $self->{data_file}, fast_analysis => $self->{fast_analysis}, model => $self->model, merge_args => $self->{merge_args} ); return $self->{log}; } sub assess_fits { my ( $self, $hash, $param, $funtype ) = @_; $funtype //= 'fit_guess'; my $errmap = $hash->{$funtype}{$param}; my @errors = map { [ $_, $errmap->{$_} ] } keys %{$errmap}; @errors = sort { $a->[1]{rmsd} <=> $b->[1]{rmsd} } @errors; my $min_err = $errors[0][1]{rmsd}; @errors = grep { $_->[1]{rmsd} <= 2 * $min_err } @errors; my @function_types = map { sprintf( '%s (%.f / %.2f%%)', $_->[0], $_->[1]{rmsd}, $_->[1]{smape} ) } @errors; return @function_types; } sub printf_aggr { my ( $self, $hash, $key, $unit ) = @_; $hash = $hash->{$key}; if ( exists $hash->{median_goodness}{smape} ) { printf( " %s: static error: %.2f%% / %.f %s (σ = %.f)\n", $key, $hash->{median_goodness}{smape}, $hash->{median_goodness}{mae}, $unit, $hash->{std_inner} ); #printf(" %s: median %.f (%.2f / %.2f%%), mean %.f (%.2f / %.2f%%), σ %.f %s\n", # $key, # $hash->{median}, # $hash->{median_goodness}{mae} // -1, # $hash->{median_goodness}{smape} // -1, # $hash->{mean}, # $hash->{mean_goodness}{mae} // -1, # $hash->{mean_goodness}{smape} // -1, # $hash->{std_inner}, # $unit #); } else { printf( " %s: static error: %.f %s (σ = %.f)\n", $key, $hash->{median_goodness}{mae}, $unit, $hash->{std_inner} ); #printf( # " %s: median %.f (%.2f), mean %.f (%.2f), σ %.f %s\n", # $key, $hash->{median}, $hash->{median_goodness}{mae}, # $hash->{mean}, $hash->{mean_goodness}{mae}, # $hash->{std_inner}, $unit #); } } sub printf_counter_status { my ( $self, $hash, $key ) = @_; $hash = $hash->{$key}; if ( 2**32 / $hash->{median} < 10e6 ) { printf( " %s: 32bit energy counter will overflow after %.f ms\n", 'power', ( 2**32 / $hash->{median} ) / 1000 ); } } sub printf_aggr_tex { my ( $self, $hash, $key, $unit, $divisor ) = @_; $hash = $hash->{$key}; if ( $unit eq 'ms' and $hash->{median} < 1e3 ) { $unit = '\us'; $divisor = 1; } elsif ( $unit eq '\uJ' and $hash->{median} < 1e6 ) { $unit = 'nJ'; $divisor = 1e3; } elsif ( $unit eq '\uW' and $hash->{median} >= 1e3 ) { $unit = 'mW'; $divisor = 1e3; } use locale; printf( ' & & \unit[%.3g]{%s}', $hash->{median} / $divisor, $unit ); } sub printf_count_tex { my ( $self, $hash, $key ) = @_; if ($hash) { $hash = $hash->{$key}; printf( ' & %d', $hash->{count} ); } else { printf(' & '); } } sub printf_eval_tex { my ( $self, $hash, $key, $unit, $divisor ) = @_; $hash = $hash->{$key}; if ( $unit eq 'ms' and $hash->{median_goodness}{mae} < 1e3 ) { $unit = '\us'; $divisor = 1; } if ( $unit eq '\uJ' and $hash->{median_goodness}{mae} < 1e6 ) { $unit = 'nJ'; $divisor = 1e3; } use locale; printf( "\n%20s & \\unit[%.3g]{%s} & \\unit[%.2g]{\\%%}", q{}, $hash->{median_goodness}{mae} / $divisor, $unit, $hash->{median_goodness}{smape} // -1 ); } sub printf_goodness { my ( $self, $modval, $hash, $key, $unit ) = @_; $hash = $hash->{$key}; if ( exists $hash->{goodness}->{smape} ) { printf( " %s: model %.f %s, log ~=%.f / µ=%.f %s, mean absolute error %.2f %s (%.2f%%)\n", $key, $modval, $unit, $hash->{median}, $hash->{mean}, $unit, $hash->{goodness}->{mae}, $unit, $hash->{goodness}{smape} ); } else { printf( " %s: model %.f %s, log ~=%.f / µ=%.f %s, mean absolute error %.2f %s\n", $key, $modval, $unit, $hash->{median}, $hash->{mean}, $unit, $hash->{goodness}->{mae}, $unit ); } } sub printf_online_goodness { my ( $self, $hash, $key, $unit ) = @_; $hash = $hash->{$key}; if ( exists $hash->{goodness}->{smape} ) { printf( " %s: ~=%.f / µ=%.f %s, mean absolute error %.2f %s (%.2f%%)\n", $key, $hash->{median}, $hash->{mean}, $unit, $hash->{goodness}->{mae}, $unit, $hash->{goodness}{smape} ); } else { printf( " %s: ~=%.f / µ=%.f %s, mean absolute error %.2f %s\n", $key, $hash->{median}, $hash->{mean}, $unit, $hash->{goodness}->{mae}, $unit ); } } sub printf_clip { my ( $self, $hash ) = @_; if ( $hash->{clip}{max} > 0.01 ) { printf( " WARNING: Up to %.f%% clipping in power measurements (avg %.f%%)" . ", results are unreliable\n", $hash->{clip}{max} * 100, $hash->{clip}{mean} * 100 ); } } sub printf_parameterized { my ( $self, $hash, $key ) = @_; $hash = $hash->{$key}; my $std_global = $hash->{std_inner}; my $std_ind_arg = $hash->{std_arg}; my $std_ind_param = $hash->{std_param}; my $std_ind_trace = $hash->{std_trace}; my $std_by_arg = $hash->{std_by_arg} // {}; my $std_by_param = $hash->{std_by_param}; my $std_by_trace = $hash->{std_by_trace} // {}; my $r_by_param = $hash->{spearmanr_by_param} // {}; my $arg_ratio; my $param_ratio; my $trace_ratio; if ( $std_global > 0 ) { $param_ratio = $std_ind_param / $std_global; if ( defined $std_ind_arg ) { $arg_ratio = $std_ind_arg / $std_global; } } if ( $std_ind_param > 0 ) { $trace_ratio = $std_ind_trace / $std_ind_param; } if ( $std_global > 10 and $param_ratio < 0.5 and not exists $hash->{function}{user} ) { printf( " %s: should be parameterized (%.2f / %.2f = %.3f)\n", $key, $std_ind_param, $std_global, $param_ratio ); } if ( ( $std_global < 10 or $param_ratio > 0.5 ) and exists $hash->{function}{user} ) { printf( " %s: should not be parameterized (%.2f / %.2f = %.3f)\n", $key, $std_ind_param, $std_global, $param_ratio ? $param_ratio : 0 ); } if ( defined $std_ind_arg and $std_global > 10 and $arg_ratio < 0.5 and not exists $hash->{function}{user_arg} ) { printf( " %s: depends on arguments (%.2f / %.2f = %.3f)\n", $key, $std_ind_arg, $std_global, $arg_ratio ); } if ( defined $std_ind_arg and ( $std_global < 10 or $arg_ratio > 0.5 ) and exists $hash->{function}{user_arg} ) { printf( " %s: should not depend on arguments (%.2f / %.2f = %.3f)\n", $key, $std_ind_arg, $std_global, $arg_ratio ? $arg_ratio : 0 ); } if ( $std_global > 10 and $trace_ratio < 0.5 ) { printf( " %s: model insufficient, depends on trace (%.2f / %.2f = %.3f)\n", $key, $std_ind_trace, $std_ind_param, $trace_ratio ); } if ( $std_global < 10 ) { return; } for my $param ( sort keys %{$std_by_param} ) { my $std_this = $std_by_param->{$param}; my $ratio = $std_ind_param / $std_this; my $status = 'does not depend'; my $fline = q{}; if ( $ratio < 0.6 ) { $status = 'might depend'; $fline = q{, probably }; $fline .= join( ' or ', $self->assess_fits( $hash, $param ) ); } if ( $ratio < 0.3 ) { $status = 'depends'; } if ($fline) { printf( " %s: %s on global %s (%.2f / %.2f = %.3f%s)\n", $key, $status, $param, $std_ind_param, $std_this, $ratio, $fline ); } if ( exists $r_by_param->{$param} ) { printf( " %s: spearman_r for global %s is %.3f (p = %.3f)\n", $key, $param, $r_by_param->{$param}, -1 ); } } for my $arg ( sort keys %{$std_by_arg} ) { my $std_this = $std_by_arg->{$arg}; my $ratio = $std_ind_arg / $std_this; my $status = 'does not depend'; my $fline = q{}; if ( $ratio < 0.6 ) { $status = 'might depend'; $fline = q{, probably }; $fline .= join( ' or ', $self->assess_fits( $hash, $arg, 'arg_fit_guess' ) ); } if ( $ratio < 0.3 ) { $status = 'depends'; } if ($fline) { printf( " %s: %s on local %s (%.2f / %.2f = %.3f%s)\n", $key, $status, $arg, $std_ind_arg, $std_this, $ratio, $fline ); } } for my $transition ( sort keys %{$std_by_trace} ) { my $std_this = $std_by_trace->{$transition}; my $ratio = $std_ind_trace / $std_this; if ( $ratio < 0.4 ) { printf( " %s: depends on presence of %s in trace (%.2f / %.2f = %.3f)\n", $key, $transition, $std_ind_trace, $std_this, $ratio ); } } } sub printf_fit { my ( $self, $hash, $key, $unit ) = @_; $hash = $hash->{$key}; for my $funtype ( sort keys %{ $hash->{function} } ) { if ( exists $hash->{function}{$funtype}{error} ) { printf( " %s: %s function could not be fitted: %s\n", $key, $funtype, $hash->{function}{$funtype}{error} ); } else { printf( " %s: %s function fit error: %.2f%% / %.f %s\n", $key, $funtype, $hash->{function}{$funtype}{fit}{smape} // -1, $hash->{function}{$funtype}{fit}{mae}, $unit ); } } for my $pair ( [ 'param_mean_goodness', 'param mean/ssr-fit' ], [ 'param_median_goodness', 'param median/static' ], [ 'arg_mean_goodness', 'arg mean/ssr-fit' ], [ 'arg_median_goodness', 'arg median/static' ] ) { my ( $goodness, $desc ) = @{$pair}; if ( exists $hash->{$goodness} ) { printf( " %s: %s LUT error: %.2f%% / %.f %s / %.f\n", $key, $desc, $hash->{$goodness}{smape} // -1, $hash->{$goodness}{mae}, $unit, $hash->{$goodness}{rmsd} ); } } } sub assess_model { my ($self) = @_; for my $name ( sort keys %{ $self->{log}{aggregate}{state} } ) { my $state = $self->{log}{aggregate}{state}{$name}; printf( "Assessing %s:\n", $name ); $self->printf_clip($state); $self->printf_aggr( $state, 'power', 'µW' ); $self->printf_counter_status( $state, 'power' ); $self->printf_parameterized( $state, 'power' ); $self->printf_fit( $state, 'power', 'µW' ); } for my $name ( sort keys %{ $self->{log}{aggregate}{transition} } ) { my $transition = $self->{log}{aggregate}{transition}{$name}; printf( "Assessing %s:\n", $name ); $self->printf_clip($transition); $self->printf_aggr( $transition, 'duration', 'µs' ); $self->printf_parameterized( $transition, 'duration' ); $self->printf_fit( $transition, 'duration', 'µs' ); $self->printf_aggr( $transition, 'energy', 'pJ' ); $self->printf_parameterized( $transition, 'energy' ); $self->printf_fit( $transition, 'energy', 'pJ' ); $self->printf_aggr( $transition, 'rel_energy_prev', 'pJ' ); $self->printf_parameterized( $transition, 'rel_energy_prev' ); $self->printf_fit( $transition, 'rel_energy_prev', 'pJ' ); if ( exists $transition->{rel_energy_next}{median} ) { $self->printf_aggr( $transition, 'rel_energy_next', 'pJ' ); $self->printf_parameterized( $transition, 'rel_energy_next' ); $self->printf_fit( $transition, 'rel_energy_next', 'pJ' ); } if ( exists $transition->{timeout}{median} ) { $self->printf_aggr( $transition, 'timeout', 'µs' ); $self->printf_parameterized( $transition, 'timeout' ); $self->printf_fit( $transition, 'timeout', 'µs' ); } } } sub assess_model_tex { my ($self) = @_; say '\begin{tabular}{|c|rrr|r|}\\hline'; say 'Zustand & $\MmedP$ & & & $n$ \\\\\\hline'; for my $name ( sort keys %{ $self->{log}{aggregate}{state} } ) { my $state = $self->{log}{aggregate}{state}{$name}; printf( "\n%20s", $name ); $self->printf_aggr_tex( $state, 'power', '\uW', 1 ); $self->printf_eval_tex( $state, 'power', '\uW', 1 ); $self->printf_count_tex( $state, 'power' ); print " \\\\"; } say '\end{tabular}\\\\'; say '\vspace{0.5cm}'; say '\begin{tabular}{|c|rr|rr|rr|r|}\\hline'; say 'Transition & & $\MmedE$ & & $\MmedF$ & & $\Mmeddur$ & $n$ \\\\\\hline'; for my $name ( sort keys %{ $self->{log}{aggregate}{transition} } ) { my $transition = $self->{log}{aggregate}{transition}{$name}; printf( "\n%20s", $name ); $self->printf_aggr_tex( $transition, 'energy', '\uJ', 1e6 ); $self->printf_aggr_tex( $transition, 'rel_energy_prev', '\uJ', 1e6 ); $self->printf_aggr_tex( $transition, 'rel_energy_next', '\uJ', 1e6 ); $self->printf_aggr_tex( $transition, 'duration', 'ms', 1e3 ); $self->printf_count_tex( $transition, 'energy' ); print " \\\\"; $self->printf_eval_tex( $transition, 'energy', '\uJ', 1e6 ); $self->printf_eval_tex( $transition, 'rel_energy_prev', '\uJ', 1e6 ); $self->printf_eval_tex( $transition, 'rel_energy_next', '\uJ', 1e6 ); $self->printf_eval_tex( $transition, 'duration', 'ms', 1e3 ); $self->printf_count_tex; print " \\\\"; } print "\\hline\n"; say '\end{tabular}'; } sub assess_workload { my ( $self, $workload ) = @_; $workload =~ s{ \s* \) \s* ; \s* }{:}gx; $workload =~ s{ \s* \) \s* $ }{}gx; $workload =~ s{ \s* ; \s* }{!:}gx; $workload =~ s{ \s* \( \s* }{!}gx; $workload =~ s{ \s* , \s* }{!}gx; $workload =~ s{ [^!] \K $ }{!}gx; say $workload; my $traces = $self->dfa->run_str_to_trace($workload); } sub update_model { my ($self) = @_; for my $name ( sort keys %{ $self->{log}{aggregate}{state} } ) { my $state = $self->{log}{aggregate}{state}{$name}; $self->model->set_state_power( $name, $state->{power}{median} ); for my $fname ( keys %{ $state->{power}{function} } ) { $self->model->set_state_params( $name, $fname, $state->{power}{function}{$fname}{raw}, @{ $state->{power}{function}{$fname}{params} } ); } if ( $self->{with_lut} ) { $self->model->set_state_lut( $name, 'power', $state->{power}{median_by_param} ); } } for my $name ( sort keys %{ $self->{log}{aggregate}{transition} } ) { my $transition = $self->{log}{aggregate}{transition}{$name}; my @keys = (qw(duration energy rel_energy_prev rel_energy_next)); if ( $self->model->get_transition_by_name($name)->{level} eq 'epilogue' ) { push( @keys, 'timeout' ); } for my $key (@keys) { $self->model->set_transition_property( $name, $key, $transition->{$key}{median} ); for my $fname ( keys %{ $transition->{$key}{function} } ) { $self->model->set_transition_params( $name, $key, $fname, $transition->{$key}{function}{$fname}{raw}, @{ $transition->{$key}{function}{$fname}{params} } ); } if ( $self->{with_lut} ) { $self->model->set_transition_lut( $name, $key, $transition->{$key}{median_by_param} ); } } } $self->model->set_voltage( $self->{log}{aggregate}{min_voltage}, $self->{log}{aggregate}{max_voltage} ); $self->model->save; } sub reset_model { my ($self) = @_; $self->model->reset; $self->model->save; } sub to_ah { my ($self) = @_; my $class_name = $self->{class_name}; my $repo = $self->repo; my $class_header = $repo->{class}{$class_name}{sources}[0]{file}; my @transition_names = grep { $_ ne q{?} } map { $_->{name} } $self->model->transitions; my $trigger_port = $self->{trigger_port}; my $trigger_pin = $self->{trigger_pin}; my $ignore_nested = q{}; my $adv_type = 'execution'; my $pass_function = $self->{logging} ? 'logTransition' : 'passTransition'; if ( $self->{ignore_nested} ) { $adv_type = 'call'; $ignore_nested = "&& !within(\"${class_name}\")"; } my $ah_buf = <<"EOF"; #ifndef ${class_name}_DFA_AH #define ${class_name}_DFA_AH #include "drivers/dfa_driver.h" #include "drivers/gpio.h" #include "drivers/eUSCI_A/uart/prototype_uart.h" #include "${class_header}" EOF if ( defined $trigger_port and defined $trigger_pin ) { $ah_buf .= "aspect ${class_name}_Trigger {\n\n"; $ah_buf .= 'pointcut Transition() = "' . join( q{" || "}, map { "% ${class_name}::$_(...)" } @transition_names ) . "\";\n\n"; $ah_buf .= <<"EOF"; advice execution("void initialize_devices()") : after() { setOutput(${trigger_port}, ${trigger_pin}); } advice ${adv_type}(Transition()) ${ignore_nested} : before() { pinHigh(${trigger_port}, ${trigger_pin}); } advice ${adv_type}(Transition()) ${ignore_nested} : after() { /* 22 = 10.2us delay @ 16MHz */ /* 32 = 14.6us delay @ 16MHz */ /* 64 = 28.6us delay @ 16MHz */ /* 160 = 50.6us delay @ 16MHz */ for (unsigned int i = 0; i < 64; i++) asm volatile("nop"); pinLow(${trigger_port}, ${trigger_pin}); } advice execution(Transition()) : order("${class_name}_DFA", "${class_name}_Trigger"); EOF if ( $self->{ignore_nested} ) { for my $transition ( $self->model->transitions ) { if ( $transition->{level} eq 'epilogue' ) { $ah_buf .= <<"EOF"; advice execution("% ${class_name}::$transition->{name}(...)") : before() { pinHigh(${trigger_port}, ${trigger_pin}); } advice execution("% ${class_name}::$transition->{name}(...)") : after() { for (unsigned int i = 0; i < 64; i++) asm volatile("nop"); pinLow(${trigger_port}, ${trigger_pin}); } EOF } } } $ah_buf .= "};\n\n"; } $ah_buf .= "aspect ${class_name}_DFA {\n\n"; for my $transition ( $self->model->transitions ) { if ( $transition->{name} ne q{?} ) { my $dest_state_id = $self->model->get_state_id( $transition->{destination} ); if ( $transition->{level} eq 'user' ) { $ah_buf .= <<"EOF"; advice ${adv_type}("% ${class_name}::$transition->{name}(...)") ${ignore_nested} : after() { tjp->target()->${pass_function}(${class_name}::statepower[tjp->target()->state], $transition->{rel_energy_prev}{static}, $transition->{id}, ${dest_state_id}); }; EOF } else { $ah_buf .= <<"EOF"; advice execution("% ${class_name}::$transition->{name}(...)") : after() { tjp->target()->${pass_function}(${class_name}::statepower[tjp->target()->state], $transition->{rel_energy_prev}{static}, $transition->{id}, ${dest_state_id}); }; EOF } } } $ah_buf .= <<"EOF"; }; #endif EOF return $ah_buf; } sub to_cc { my ($self) = @_; my $class_name = $self->{class_name}; my @state_enum = $self->model->get_state_enum; my %param_default; for my $default_setting ( @{ $self->{param_default} } ) { my ( $param, $value ) = split( qr{ = }x, $default_setting ); $param_default{$param} = $value; } my $buf = "DFA_Driver::power_uW_t ${class_name}::statepower[] = {" . join( ', ', map { sprintf( '%.f', $self->model->get_state_power_with_params( $_, \%param_default ) ) } @state_enum ) . "};\n"; return $buf; } sub to_h { my ($self) = @_; my @state_enum = $self->model->get_state_enum; my $buf = "public:\n" . "static power_uW_t statepower[];\n" . "enum State : uint8_t {" . join( ', ', @state_enum ) . "};\n"; return $buf; } sub to_tikz { my ($self) = @_; my $buf = <<'EOF'; \begin{tikzpicture}[node distance=3cm,>=stealth',bend angle=45,auto,->] \tikzstyle{state}=[ellipse,thick,draw=black!75,minimum size=1cm,inner sep=2pt] EOF my @state_enum = $self->model->get_state_enum; my $initial = shift(@state_enum); my $prev = $initial; my $ini_name = $initial; if ( $ini_name eq 'UNINITIALIZED' ) { $ini_name = '?'; } $buf .= "\t\t\\node [state,initial,initial text={},initial where=left] ($initial) {\\small $ini_name};\n"; for my $state (@state_enum) { $buf .= "\t\t\\node [state,right of=${prev}] ($state) {\\small $state};\n"; $prev = $state; } $buf .= "\n\t\t\\path\n"; for my $transition ( $self->model->transitions ) { for my $transition_elem ( @{ $transition->{transitions} } ) { my ( $origin, $destination ) = @{$transition_elem}; my @edgestyles; if ( $transition->{level} eq 'epilogue' ) { push( @edgestyles, 'dashed' ); } if ( $origin eq $destination ) { push( @edgestyles, 'loop above' ); } my $edgestyle = @edgestyles ? '[' . join( q{,}, @edgestyles ) . ']' : q{}; $buf .= "\t\t ($origin) edge ${edgestyle} node {$transition->{name}} ($destination)\n"; } } $buf .= "\t\t;\n"; $buf .= "\t\\end{tikzpicture}\n"; return $buf; } sub to_test_ah { my ($self) = @_; my $buf = <<"EOF"; /* * Autogenerated code -- Manual changes are not preserved * vim:readonly */ #ifndef DRIVEREVAL_AH #define DRIVEREVAL_AH #include "DriverEval.h" #include "syscall/guarded_scheduler.h" aspect StartDFADriverEvalThread { advice execution("void ready_threads()") : after() { organizer.Scheduler::ready(driverEvalThread); } }; #endif EOF return $buf; } sub to_test_cc { my ($self) = @_; my $class_name = $self->{class_name}; my @runs = $self->dfa->traces; my @state_enum = $self->model->get_state_enum; my $dfa = $self->dfa->dfa; my $num_runs = @runs; my $instance = $self->repo->get_class_instance( $self->{class_name} ); my $state_duration = $self->{state_duration} // 1000; my $buf = <<"EOF"; /* * Autogenerated code - Manual changes are not preserved. * vim:readonly */ #include "DriverEval.h" #include "syscall/guarded_buzzer.h" DeclareThread(DriverEvalThread_${class_name}, driverEvalThread, 256); EOF $buf .= <<"EOF"; void DriverEvalThread_${class_name}::action() { Guarded_Buzzer buzzer; while (1) { /* wait for MIMOSA calibration */ buzzer.sleep(12000); buzzer.set(${state_duration}); EOF $buf .= "${instance}.startIteration(${num_runs});\n"; for my $run (@runs) { $buf .= "\t\t/* test run $run->{id} start */\n"; $buf .= "\t\t${instance}.resetLogging();\n"; # $buf .= "\t\t${instance}.resetAccounting();\n"; # TODO sinnvoll? my $state = 0; for my $transition ( grep { $_->{isa} eq 'transition' } @{ $run->{trace} } ) { my ( $cmd, @args ) = @{ $transition->{code} }; my ($new_state) = $dfa->successors( $state, ":${cmd}!" . join( '!', @args ) ); my $state_name = $self->dfa->reduced_id_to_state($state); my $new_state_name = $self->dfa->reduced_id_to_state($new_state); $buf .= "\t\t/* Transition $state_name -> $new_state_name */\n"; if ( $self->model->get_transition_by_name($cmd)->{level} eq 'epilogue' ) { $buf .= "\t\t/* wait for $cmd interrupt */\n"; $buf .= "\t\tbuzzer.sleep();\n"; } else { $buf .= sprintf( "\t\t%s.%s(%s);\n", $instance, $cmd, join( ', ', @args ) ); $buf .= "\t\tbuzzer.sleep();\n"; } $buf .= $self->model->after_transition_code; $state = $new_state; } $buf .= "\t\t${instance}.dumpLog();\n\n"; } $buf .= "${instance}.stopIteration(); }}\n"; return $buf; } sub to_test_h { my ($self) = @_; my $class_name = $self->{class_name}; my $class_prefix = $self->repo->get_class_path_prefix($class_name); my $buf = <<"EOF"; /* * Autogenerated code -- Manual changes are not preserved * vim:readonly */ #ifndef DRIVEREVAL_H #define DRIVEREVAL_H #include "${class_prefix}.h" #include "syscall/thread.h" class DriverEvalThread_${class_name} : public Thread { public: DriverEvalThread_${class_name}(void* tos) : Thread(tos) { } void action(); }; extern DriverEvalThread_${class_name} driverEvalThread; #endif EOF return $buf; } sub to_test_json { my ($self) = @_; return JSON->new->encode( [ $self->dfa->traces ] ); } sub rm_acc_files { my ($self) = @_; for my $file ( $self->{ah_file}, $self->{cc_file}, $self->{h_file} ) { if ( -e $file ) { unlink($file); } } return $self; } sub write_test_files { my ($self) = @_; my $prefix = $self->{prefix} . '/apps/DriverEval'; if ( not -d $prefix ) { mkdir($prefix); } write_file( "${prefix}/DriverEval.ah", $self->to_test_ah ); write_file( "${prefix}/DriverEval.cc", $self->to_test_cc ); write_file( "${prefix}/DriverEval.h", $self->to_test_h ); write_file( "${prefix}/DriverEval.json", $self->to_test_json ); # Old log may no longer apply to new test files unlink("${prefix}/DriverLog.txt"); return $self; } sub rm_test_files { my ($self) = @_; my $prefix = $self->{prefix} . '/apps/DriverEval/DriverEval'; for my $file ( "${prefix}.ah", "${prefix}.cc", "${prefix}.h" ) { if ( -e $file ) { unlink($file); } } return $self; } sub archive_files { my ($self) = @_; $self->{lp}{timestamp} //= DateTime->now( time_zone => 'Europe/Berlin' ) ->strftime('%Y%m%d_%H%M%S'); my $tar = Archive::Tar->new; my @eval_files = ( ( map { "src/apps/DriverEval/DriverEval.$_" } (qw(ah cc h json)) ), ( map { "src/apps/DriverEval/DriverLog.$_" } (qw(json txt)) ), ); my @mim_files = grep { m{ \. mim }x } read_dir('.'); $tar->add_files( $self->{model_file}, @eval_files, @mim_files ); $tar->add_data( 'setup.json', JSON->new->encode( { excluded_states => $self->{excluded_states}, ignore_nested => $self->{ignore_nested}, mimosa_offset => $self->{mimosa_offset}, mimosa_shunt => $self->{mimosa_shunt}, mimosa_voltage => $self->{mimosa_voltage}, state_duration => $self->{state_duration}, trace_filter => $self->{trace_filter}, trace_revisit => $self->{trace_revisit}, trigger_pin => $self->{trigger_pin}, trigger_port => $self->{trigger_port}, } ) ); my $filename = "../data/$self->{lp}{timestamp}_$self->{class_name}"; if ( $self->{filename_suffix} ) { $filename .= '_' . $self->{filename_suffix}; } $filename .= '.tar'; $tar->write($filename); return $self; } sub write_acc_files { my ($self) = @_; write_file( $self->{ah_file}, $self->to_ah ); write_file( $self->{cc_file}, $self->to_cc ); write_file( $self->{h_file}, $self->to_h ); return $self; } sub launchpad_connect { my ($self) = @_; $self->{port_file} //= '/dev/ttyACM1'; $self->{port} = Device::SerialPort->new( $self->{port_file} ) or croak("Error openig serial port $self->{port_file}"); $self->{port}->baudrate( $self->{baud_rate} // 115200 ); $self->{port}->databits(8); $self->{port}->parity('none'); $self->{port}->read_const_time(500); return $self; } sub launchpad_flash { my ($self) = @_; my ( $make_buf, $prog_buf ); my $remake = harness( [ 'make', '-B' ], '<' => \undef, '>&' => \$make_buf, ); my $make_program = harness( [ 'make', 'program' ], '<' => \undef, '>&' => \$prog_buf, ); $remake->run or croak( 'make -B returned ' . $remake->full_result ); $make_program->run or croak( 'make program returned ' . $remake->full_result ); return $self; } sub launchpad_reset { my ($self) = @_; my $output_buffer; my $make_reset = harness( [ 'make', 'reset' ], '<' => \undef, '>&' => \$output_buffer, ); $make_reset->run or croak( 'make reset returned ' . $make_reset->full_result ); return $self; } sub launchpad_log_clean { my ($self) = @_; for my $file ( read_dir('.') ) { if ( $file =~ m{ \. mim $ }x ) { unlink($file); } } } sub launchpad_log_init { my ($self) = @_; $self->{lp}{run_id} = 0; $self->{lp}{sync} = 0; $self->{lp}{calibrating} = 0; $self->{lp}{run_done} = 0; $self->{lp}{run} = []; $self->{lp}{log} = []; $self->{lp}{errors} = []; $self->{lp}{log_buf} = q{}; $self->{lp}{re}{iter_start} = qr{ ^ \[ EP \] \s iteration \s start, \s (? \d+ ) \s runs $ }x; $self->{lp}{re}{iter_stop} = qr{ ^ \[ EP \] \s iteration \s stop $ }x; $self->{lp}{re}{run_start} = qr{ ^ \[ EP \] \s run \s start $ }x; $self->{lp}{re}{run_stop} = qr{ ^ \[ EP \] \s run \s stop, \s energyUsed = (? \S+) $ }x; $self->{lp}{re}{transition} = qr{ ^ \[ EP \] \s dt = (? \S+) \s de = (? \S+) \s oldst = (? \S+ ) \s trid = (? \S+ ) $ }x; $self->launchpad_connect; return $self; } sub launchpad_run_done { my ($self) = @_; if ( $self->{lp}{run_done} ) { $self->{lp}{run_done} = 0; return 1; } return 0; } sub launchpad_get_errors { my ($self) = @_; my @errors = @{ $self->{lp}{errors} }; $self->{lp}{errors} = []; return @errors; } sub launchpad_log_is_synced { my ($self) = @_; return $self->{lp}{sync}; } sub launchpad_log_status { my ($self) = @_; return ( $self->{lp}{iteration}, $self->{lp}{run_id}, $self->{lp}{num_runs} ); } sub launchpad_log_read { my ($self) = @_; my $port = $self->{port}; my ( $count, $chars ) = $port->read(1024); $self->{lp}{log_buf} .= $chars; if ( not defined $count ) { $port->close; croak("Serial port was disconnected"); } if ( $count > 0 ) { my @lines = split( /\n\r|\r\n/, $chars ); for my $line (@lines) { $self->launchpad_parse_line($line); } } } sub merged_json { my ($self) = @_; my @traces = $self->dfa->traces; for my $run ( @{ $self->{lp}{log} } ) { my $trace_idx = $run->{id} - 1; my $idx = 0; assert_is( $traces[$trace_idx]{id}, $run->{id} ); push( @{ $traces[$trace_idx]{total_energy} }, $run->{total_energy} ); for my $online_obj ( @{ $run->{trace} } ) { my $plan_obj = $traces[$trace_idx]{trace}[$idx]; #printf("%-15s %-15s\n", $plan_obj->{name}, $online_obj->{name}); if ( not defined $plan_obj->{name} ) { # The planned test run is done, but the hardware reported an # epilogue-level transition before the next run was started. $traces[$trace_idx]{trace}[$idx] = { isa => $online_obj->{isa}, name => $online_obj->{name}, parameter => $traces[$trace_idx]{trace}[ $idx - 1 ]{parameter}, }; if ( exists $traces[$trace_idx]{trace}[ $idx - 1 ] {final_parameter} ) { $traces[$trace_idx]{trace}[$idx]{parameter} = $traces[$trace_idx]{trace}[ $idx - 1 ]{final_parameter}; } } else { if ( $online_obj->{isa} ne $plan_obj->{isa} ) { printf( "Log merge: ISA mismatch (should be %s, is %s) at index %d#%d\n", $plan_obj->{isa}, $online_obj->{isa}, $trace_idx, $idx ); $self->mimosa->kill; exit(1); } if ( $plan_obj->{name} ne 'UNINITIALIZED' ) { if ( $online_obj->{name} ne $plan_obj->{name} ) { printf( "Log merge: name mismatch (should be %s, is %s) at index %d#%d\n", $plan_obj->{name}, $online_obj->{name}, $trace_idx, $idx ); $self->mimosa->kill; exit(1); } } } push( @{ $traces[$trace_idx]{trace}[$idx]{online} }, $online_obj->{online} ); $idx++; } } $self->{lp}{log} = []; return @traces; } sub launchpad_parse_line { my ( $self, $line ) = @_; if ( $line =~ $self->{lp}{re}{iter_start} ) { $self->{lp}{sync} = 1; $self->{lp}{run_id} = 0; $self->{lp}{num_runs} = $+{runs}; $self->{lp}{calibrating} = 0; } elsif ( not $self->{lp}{sync} ) { return; } elsif ( $line =~ $self->{lp}{re}{iter_stop} ) { $self->{lp}{iteration}++; $self->{lp}{calibrating} = 1; write_file( '../kratos/src/apps/DriverEval/DriverLog.txt', $self->{lp}{log_buf} ); write_file( '../kratos/src/apps/DriverEval/DriverLog.json', JSON->new->encode( [ $self->merged_json ] ) ); } elsif ( $line =~ $self->{lp}{re}{run_start} ) { $self->{lp}{run_id}++; $self->{lp}{run} = []; } elsif ( $line =~ $self->{lp}{re}{run_stop} ) { $self->{lp}{run_done} = 1; push( @{ $self->{lp}{log} }, { id => $self->{lp}{run_id}, trace => [ @{ $self->{lp}{run} } ], total_energy => 0 + $+{total_e}, } ); } elsif ( $line =~ $self->{lp}{re}{transition} ) { push( @{ $self->{lp}{run} }, { isa => 'state', name => ( $self->model->get_state_enum )[ $+{old_state} ], online => { time => 0 + $+{delta_t}, energy => 0 + $+{delta_e}, power => 0 + $+{delta_e} / $+{delta_t}, }, }, { isa => 'transition', name => $self->model->get_transition_by_id( $+{transition_id} ) ->{name}, online => { timeout => 0 + $+{delta_t}, }, }, ); } else { $self->{lp}{sync} = 0; push( @{ $self->{lp}{errors} }, "Cannot parse $line" ); } } 1;