diff --git a/.bin/Scripts/functions/hw_diags.py b/.bin/Scripts/functions/hw_diags.py index dce6f140..f173b6f6 100644 --- a/.bin/Scripts/functions/hw_diags.py +++ b/.bin/Scripts/functions/hw_diags.py @@ -27,6 +27,30 @@ ATTRIBUTES = { 201: {'Warning': 1}, }, } +IO_VARS = { + 'Block Size': 512*1024, + 'Chunk Size': 16*1024**2, + 'Minimum Dev Size': 8*1024**3, + 'Minimum Test Size': 10*1024**3, + 'Alt Test Size Factor': 0.01, + 'Progress Refresh Rate': 5, + 'Scale 16': [2**(0.6*x)+(16*x) for x in range(1,17)], + 'Scale 32': [2**(0.6*x/2)+(16*x/2) for x in range(1,33)], + 'Threshold Fail': 65*1024**2, + 'Threshold Warn': 135*1024**2, + 'Threshold Great': 750*1024**2, + 'Graph Horizontal': ('▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'), + 'Graph Horizontal Width': 40, + 'Graph Vertical': ( + '▏', '▎', '▍', '▌', + '▋', '▊', '▉', '█', + '█▏', '█▎', '█▍', '█▌', + '█▋', '█▊', '█▉', '██', + '██▏', '██▎', '██▍', '██▌', + '██▋', '██▊', '██▉', '███', + '███▏', '███▎', '███▍', '███▌', + '███▋', '███▊', '███▉', '████'), + } TESTS = { 'Prime95': { 'Enabled': False, @@ -49,6 +73,45 @@ TESTS = { }, } +def generate_horizontal_graph(rates): + """Generate two-line horizontal graph from rates, returns str.""" + line_top = '' + line_bottom = '' + for r in rates: + step = get_graph_step(r, scale=16) + + # Set color + r_color = COLORS['CLEAR'] + if r < IO_VARS['Threshold Fail']: + r_color = COLORS['RED'] + elif r < IO_VARS['Threshold Warn']: + r_color = COLORS['YELLOW'] + elif r > IO_VARS['Threshold Great']: + r_color = COLORS['GREEN'] + + # Build graph + if step < 8: + line_top += ' ' + line_bottom += '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][step]) + else: + line_top += '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][step-8]) + line_bottom += '{}{}'.format(r_color, IO_VARS['Graph Horizontal'][-1]) + line_top += COLORS['CLEAR'] + line_bottom += COLORS['CLEAR'] + return '{}\n{}'.format(line_top, line_bottom) + +def get_graph_step(rate, scale=16): + """Get graph step based on rate and scale, returns int.""" + m_rate = rate / (1024**2) + step = 0 + scale_name = 'Scale {}'.format(scale) + for x in range(scale-1, -1, -1): + # Iterate over scale backwards + if m_rate >= IO_VARS[scale_name][x]: + step = x + break + return step + def get_read_rate(s): """Get read rate in bytes/s from dd progress output.""" real_rate = None @@ -254,28 +317,113 @@ def run_iobenchmark(): TESTS['iobenchmark']['Status'][name] = 'Working' update_progress() print_standard(' /dev/{:11} '.format(name+'...'), end='', flush=True) - run_program('tmux split-window -dl 5 {} {} {}'.format( - 'hw-diags-iobenchmark', - '/dev/{}'.format(name), - progress_file).split()) - wait_for_process('dd') + + # Get dev size + cmd = 'sudo lsblk -bdno size /dev/{}'.format(name) + try: + result = run_program(cmd.split()) + dev_size = result.stdout.decode().strip() + dev_size = int(dev_size) + except: + # Failed to get dev size, requires manual testing instead + TESTS['iobenchmark']['Status'][name] = 'Unknown' + continue + if dev_size < IO_VARS['Minimum Dev Size']: + TESTS['iobenchmark']['Status'][name] = 'Unknown' + continue + + # Calculate dd values + ## test_size is the area to be read in bytes + ## If the dev is < 10Gb then it's the whole dev + ## Otherwise it's the smaller of 10Gb and 1% of the dev + ## + ## test_chunks is the number of groups of "Chunk Size" in test_size + ## This number is reduced to a multiple of the graph width in + ## order to allow for the data to be condensed cleanly + ## + ## skip_blocks is the number of "Block Size" groups not tested + ## skip_count is the number of blocks to skip per test_chunk + ## skip_extra is how often to add an additional skip block + ## This is needed to ensure an even testing across the dev + ## This is calculated by using the fractional amount left off + ## of the skip_count variable + test_size = min(IO_VARS['Minimum Test Size'], dev_size) + test_size = max( + test_size, dev_size*IO_VARS['Alt Test Size Factor']) + test_chunks = int(test_size // IO_VARS['Chunk Size']) + test_chunks -= test_chunks % IO_VARS['Graph Horizontal Width'] + test_size = test_chunks * IO_VARS['Chunk Size'] + skip_blocks = int((dev_size - test_size) // IO_VARS['Block Size']) + skip_count = int((skip_blocks / test_chunks) // 1) + skip_extra = 0 + try: + skip_extra = 1 + int(1 / ((skip_blocks / test_chunks) % 1)) + except ZeroDivisionError: + # skip_extra == 0 is fine + pass + + # Open dd progress pane after initializing file + with open(progress_file, 'w') as f: + f.write('') + sleep(1) + cmd = 'tmux split-window -dp 75 -PF #D tail -f {}'.format( + progress_file) + result = run_program(cmd.split()) + bottom_pane = result.stdout.decode().strip() + + # Run dd read tests + offset = 0 + read_rates = [] + for i in range(test_chunks): + i += 1 + s = skip_count + c = int(IO_VARS['Chunk Size'] / IO_VARS['Block Size']) + if skip_extra and i % skip_extra == 0: + s += 1 + cmd = 'sudo dd bs={b} skip={s} count={c} if=/dev/{n} of={o}'.format( + b=IO_VARS['Block Size'], + s=offset+s, + c=c, + n=name, + o='/dev/null') + result = run_program(cmd.split()) + result_str = result.stderr.decode().replace('\n', '') + read_rates.append(get_read_rate(result_str)) + if i % IO_VARS['Progress Refresh Rate'] == 0: + # Update vertical graph + update_io_progress( + percent=i/test_chunks*100, + rate=read_rates[-1], + progress_file=progress_file) + # Update offset + offset += s + c print_standard('Done', timestamp=False) - # Check results - with open(progress_file, 'r') as f: - text = f.read() - io_stats = text.replace('\r', '\n').split('\n') - try: - io_stats = [get_read_rate(s) for s in io_stats] - io_stats = [float(s/1048576) for s in io_stats if s] - TESTS['iobenchmark']['Results'][name] = 'Read speed: {:3.1f} MB/s (Min: {:3.1f}, Max: {:3.1f})'.format( - sum(io_stats) / len(io_stats), - min(io_stats), - max(io_stats)) - TESTS['iobenchmark']['Status'][name] = 'CS' - except: - # Requires manual testing - TESTS['iobenchmark']['Status'][name] = 'NS' + # Close bottom pane + run_program(['tmux', 'kill-pane', '-t', bottom_pane]) + + # Build report + h_graph_rates = [] + pos = 0 + width = int(test_chunks / IO_VARS['Graph Horizontal Width']) + for i in range(IO_VARS['Graph Horizontal Width']): + # Append average rate for WIDTH number of rates to new array + h_graph_rates.append(sum(read_rates[pos:pos+width])/width) + pos += width + report = generate_horizontal_graph(h_graph_rates) + report += '\nRead speed: {:3.1f} MB/s (Min: {:3.1f}, Max: {:3.1f})'.format( + sum(read_rates)/len(read_rates)/1024**2, + min(read_rates)/1024**2, + max(read_rates)/1024**2) + TESTS['iobenchmark']['Results'][name] = report + + # Set CS/NS + if min(read_rates) <= IO_VARS['Threshold Fail']: + TESTS['iobenchmark']['Status'][name] = 'NS' + elif min(read_rates) <= IO_VARS['Threshold Warn']: + TESTS['iobenchmark']['Status'][name] = 'Unknown' + else: + TESTS['iobenchmark']['Status'][name] = 'CS' # Move temp file shutil.move(progress_file, '{}/iobenchmark-{}.log'.format( @@ -759,13 +907,38 @@ def show_results(): and io_status not in ['Denied', 'OVERRIDE', 'Skipped']): print_info('Benchmark:') result = TESTS['iobenchmark']['Results'].get(name, '') - print_standard(' {}'.format(result)) + for line in result.split('\n'): + print_standard(' {}'.format(line)) print_standard(' ') # Done pause('Press Enter to return to main menu... ') run_program('tmux kill-pane -a'.split()) +def update_io_progress(percent, rate, progress_file): + """Update I/O progress file.""" + bar_color = COLORS['CLEAR'] + rate_color = COLORS['CLEAR'] + step = get_graph_step(rate, scale=32) + if rate < IO_VARS['Threshold Fail']: + bar_color = COLORS['RED'] + rate_color = COLORS['YELLOW'] + elif rate < IO_VARS['Threshold Warn']: + bar_color = COLORS['YELLOW'] + rate_color = COLORS['YELLOW'] + elif rate > IO_VARS['Threshold Great']: + bar_color = COLORS['GREEN'] + rate_color = COLORS['GREEN'] + line = ' {p:5.1f}% {b_color}{b:<4} {r_color}{r:6.1f} Mb/s{c}\n'.format( + p=percent, + b_color=bar_color, + b=IO_VARS['Graph Vertical'][step], + r_color=rate_color, + r=rate/(1024**2), + c=COLORS['CLEAR']) + with open(progress_file, 'a') as f: + f.write(line) + def update_progress(): """Update progress file.""" if 'Progress Out' not in TESTS: @@ -821,3 +994,4 @@ def update_progress(): if __name__ == '__main__': print("This file is not meant to be called directly.") +# vim: sts=4 sw=4 ts=4