555 lines
17 KiB
Ruby
Executable File
555 lines
17 KiB
Ruby
Executable File
# File:: disktools.rb
|
|
# Description:: statistically analyses the 360 disk emulation output
|
|
#
|
|
# Author:: Derek Ward <derek.ward@rockstarnorth.com>
|
|
# Date:: 04 Septmeber 2009
|
|
#
|
|
|
|
#-----------------------------------------------------------------------------
|
|
# Uses
|
|
#-----------------------------------------------------------------------------
|
|
require 'win32ole'
|
|
require 'pipeline/os/path'
|
|
require 'time'
|
|
require 'pipeline/os/getopt'
|
|
require 'pipeline/os/path'
|
|
require 'fileutils'
|
|
include FileUtils
|
|
require 'dl'
|
|
include Pipeline
|
|
require 'pipeline/log/log'
|
|
|
|
#-----------------------------------------------------------------------------
|
|
# Constants
|
|
#-----------------------------------------------------------------------------
|
|
|
|
OPTIONS = [
|
|
[ '--xlsfilename', '-x', Getopt::REQUIRED, 'specify xls filename to process' ],
|
|
[ '--trylayerswap', '-l', Getopt::REQUIRED, 'tries to optimise for layer swapping' ],
|
|
[ '--checkthruput', '-t', Getopt::OPTIONAL, 'check the throughput of data' ]
|
|
]
|
|
|
|
BUTTONS_OK = 0
|
|
BUTTONS_OKCANCEL = 1
|
|
BUTTONS_ABORTRETRYIGNORE = 2
|
|
BUTTONS_YESNO = 4
|
|
|
|
CLICKED_OK = 1
|
|
CLICKED_CANCEL = 2
|
|
CLICKED_ABORT = 3
|
|
CLICKED_RETRY = 4
|
|
CLICKED_IGNORE = 5
|
|
CLICKED_YES = 6
|
|
CLICKED_NO = 7
|
|
|
|
ICON_HAND = 16
|
|
ICON_QUESTION = 32
|
|
ICON_EXCLAMATION = 48
|
|
ICON_ASTERISK = 64
|
|
|
|
#-----------------------------------------------------------------------------
|
|
# Implementation
|
|
#-----------------------------------------------------------------------------
|
|
module DiskTools
|
|
#---------------------------------------------------------------------
|
|
def DiskTools::message_box(txt='', title='', buttons=0, icon=0)
|
|
user32 = DL.dlopen('user32')
|
|
msgbox = user32['MessageBoxA', 'ILSSI']
|
|
r, rs = msgbox.call(0, txt, title, buttons+icon)
|
|
return r
|
|
end
|
|
|
|
#---------------------------------------------------------------------
|
|
# class repository for Excel constants involved in Win32OLE calls.
|
|
class ExcelConst
|
|
def ExcelConst::filled=(val)
|
|
@@filled = val
|
|
end
|
|
def ExcelConst::filled()
|
|
@@filled
|
|
end
|
|
private
|
|
@@filled = false
|
|
end
|
|
|
|
#---------------------------------------------------------------------
|
|
# class repository for MSO constants involved in Win32OLE calls.
|
|
class MSOConst
|
|
def MSOConst::filled=(val)
|
|
@@filled = val
|
|
end
|
|
def MSOConst::filled()
|
|
@@filled
|
|
end
|
|
private
|
|
@@filled = false
|
|
end
|
|
|
|
#---------------------------------------------------------------------
|
|
class DiskFiles
|
|
attr_accessor :files
|
|
def initialize()
|
|
@files = {}
|
|
end
|
|
|
|
def pretty_print()
|
|
puts "\nDISK FILES (#{@files.length})"
|
|
@files.each { |key, file| file.pretty_print }
|
|
end
|
|
end
|
|
|
|
#---------------------------------------------------------------------
|
|
class DiskFile
|
|
attr_accessor :filename
|
|
attr_accessor :size
|
|
attr_accessor :layer
|
|
attr_accessor :layer_swaps
|
|
attr_accessor :locked
|
|
attr_accessor :times_read
|
|
attr_accessor :total_seek
|
|
attr_accessor :total_read
|
|
attr_accessor :total_blocks
|
|
attr_accessor :total_actual
|
|
|
|
def initialize(filename,size,layer,layer_swaps,locked,times_read,total_seek,total_read,total_blocks,total_actual)
|
|
@filename = filename
|
|
@size = size
|
|
@layer = layer
|
|
@layer_swaps = layer_swaps
|
|
@locked = locked
|
|
@times_read = times_read
|
|
@total_seek = total_seek
|
|
@total_read = total_read
|
|
@total_blocks = total_blocks
|
|
@total_actual = total_actual
|
|
end
|
|
|
|
def pretty_print(title="Files", msgbox = false)
|
|
fn = filename[0,60]
|
|
fn = fn.ljust(60)
|
|
siz = @size.to_s.ljust(10)
|
|
ls = @layer_swaps.to_s.ljust(4)
|
|
tr = @times_read.to_i.to_s.ljust(5)
|
|
tots = @total_seek.to_i.to_s.ljust(10)
|
|
totr = @total_read.to_i.to_s.ljust(10)
|
|
totb = @total_blocks.to_s.ljust(5)
|
|
tota = @total_actual.to_i.to_s.ljust(10)
|
|
str = "#{fn} Lyr #{@layer} LyrSwap #{ls} xRead #{tr} Seek #{tots} Read #{totr} Blks #{totb} Act #{tota}"
|
|
puts str
|
|
DiskTools::message_box( str, title, BUTTONS_OK, ICON_EXCLAMATION) if msgbox
|
|
end
|
|
end
|
|
|
|
#---------------------------------------------------------------------
|
|
#Time(ms) Operation Seek(us) Read(us) Lyr Start LBA Blocks Actual(us) File(s)Accessed
|
|
class DiskEvent
|
|
attr_accessor :time
|
|
attr_accessor :operation
|
|
attr_accessor :seek
|
|
attr_accessor :read
|
|
attr_accessor :layer
|
|
attr_accessor :start_lba
|
|
attr_accessor :blocks
|
|
attr_accessor :actual
|
|
attr_accessor :filename
|
|
attr_accessor :locked
|
|
attr_accessor :dest_layer
|
|
def initialize(time,operation,seek,read,layer,start_lba,blocks,actual,filename,locked,dest_layer)
|
|
@time = time
|
|
@operation = operation
|
|
@seek = seek
|
|
@read = read
|
|
@layer = layer
|
|
@start_lba = start_lba
|
|
@blocks = blocks
|
|
@actual = actual
|
|
@filename = filename
|
|
@locked = locked
|
|
@dest_layer = dest_layer
|
|
end
|
|
|
|
def pretty_print()
|
|
puts "time #{@time} operation #{@operation} seek #{@seek} read #{@read} layer #{@layer} start_lba #{@start_lba} blocks #{@blocks} actual #{@actual} filename #{@filename} locked #{@locked} dest_layer #{@dest_layer}"
|
|
end
|
|
end
|
|
|
|
#---------------------------------------------------------------------
|
|
class DiskEvents
|
|
attr_accessor :events
|
|
|
|
def initialize()
|
|
@events = Array.new
|
|
end
|
|
|
|
def pretty_print()
|
|
puts "\nDISK EVENTS (#{@events.length})"
|
|
@events.each { |de| de.pretty_print }
|
|
end
|
|
end
|
|
|
|
#---------------------------------------------------------------------
|
|
class Stats
|
|
attr_accessor :total_layer_flips
|
|
|
|
def initialize(events, files)
|
|
@total_layer_flips = 0
|
|
len = events.events.length
|
|
|
|
events.events.each_with_index do |event, index|
|
|
curr_layer = event.layer
|
|
prev_layer = event.layer
|
|
next_layer = event.layer
|
|
prev_layer = events.events[index-1].layer if index > 0
|
|
next_layer = events.events[index+1].layer if index < len-2
|
|
|
|
@total_layer_flips += 1 if next_layer != curr_layer
|
|
end
|
|
end
|
|
|
|
def pretty_print(title = "Stats", msgbox = true)
|
|
str = "\nTotal Layer Flips #{@total_layer_flips} (#{total_layer_flips*75}ms)"
|
|
puts str
|
|
DiskTools::message_box( str, title, BUTTONS_OK, ICON_EXCLAMATION) if msgbox
|
|
end
|
|
end
|
|
|
|
#---------------------------------------------------------------------
|
|
# == Description
|
|
# read the whole worksheet into my diskevents class
|
|
def DiskTools::ReadSheet(worksheet, disk_events)
|
|
data = worksheet.UsedRange.Value
|
|
num_rows = data.size
|
|
num_columns = data[0].size
|
|
puts "num rows #{num_rows} num columns #{num_columns}\n"
|
|
|
|
row = 1
|
|
column = 1
|
|
|
|
num_columns.times { |c| puts worksheet.cells(row,column+c)['Value'] }
|
|
|
|
row += 1
|
|
|
|
max_lines_to_read = 99999
|
|
|
|
puts "Interpreting worksheet...#{max_lines_to_read} lines"
|
|
# Nuts... this is really too slow in ruby
|
|
max_lines_to_read.times do
|
|
val = worksheet.cells(row, column)['Value']
|
|
break unless val
|
|
|
|
#hh:mm:ss:msec, READ, LBA, Count, Read(ms), Seek(ms), Actual(ms), Emulator Delay(ms), Files...
|
|
#00:00:01:522.1, READ, 32, 16, 5.6, 80.6, 86.2, -0.0, <volume descriptor>, <empty>
|
|
time = worksheet.cells(row, column)['Value']
|
|
operation = worksheet.cells(row, column+1)['Value']
|
|
start_lba = worksheet.cells(row, column+2)['Value'].to_i
|
|
blocks = worksheet.cells(row, column+3)['Value'].to_i
|
|
read = worksheet.cells(row, column+4)['Value'].to_f
|
|
seek = worksheet.cells(row, column+5)['Value'].to_f
|
|
actual = worksheet.cells(row, column+6)['Value'].to_f
|
|
emu_delay = worksheet.cells(row, column+7)['Value'].to_f
|
|
filename = worksheet.cells(row, column+8)['Value']
|
|
layer = 0 # ahh boo why do they no longer give you this?
|
|
|
|
times = time.split(":")
|
|
time = times[0].to_i * 60 * 60 * 1000
|
|
time += times[1].to_i * 60 * 1000
|
|
time += times[2].to_i * 1000
|
|
|
|
times2 = times[3].split(".")
|
|
time += times2[0].to_i
|
|
|
|
#Time(ms) Operation Seek(us) Read(us) Lyr Start LBA Blocks Actual(us) File(s)Accessed
|
|
#time = worksheet.cells(row, column)['Value'].to_i
|
|
#operation = worksheet.cells(row, column+1)['Value']
|
|
#seek = worksheet.cells(row, column+2)['Value'].to_i
|
|
#read = worksheet.cells(row, column+3)['Value'].to_i
|
|
#layer = worksheet.cells(row, column+4)['Value'].to_i
|
|
#start_lba = worksheet.cells(row, column+5)['Value'].to_i
|
|
#blocks = worksheet.cells(row, column+6)['Value'].to_i
|
|
#actual = worksheet.cells(row, column+7)['Value'].to_i
|
|
#filename = worksheet.cells(row, column+8)['Value']
|
|
locked = 0#worksheet.cells(row, column+9)['Value'].to_i
|
|
dest_layer = 0#worksheet.cells(row, column+10)['Value'].to_i
|
|
|
|
disk_events.events << DiskEvent.new(time,operation,seek,read,layer,start_lba,blocks,actual,filename,locked,dest_layer)
|
|
|
|
row += 1
|
|
end
|
|
puts "Interpreting worksheet complete"
|
|
end
|
|
|
|
|
|
#
|
|
# == Description
|
|
def DiskTools::BuildFiles(events, files)
|
|
layer_swaps = 0
|
|
len = events.events.length
|
|
|
|
events.events.each_with_index do |event, index|
|
|
if files.files.has_key?(event.filename) then
|
|
files.files[event.filename].times_read += 1
|
|
files.files[event.filename].total_seek += event.seek
|
|
files.files[event.filename].total_read += event.read
|
|
files.files[event.filename].total_blocks += event.blocks
|
|
files.files[event.filename].total_actual += event.actual
|
|
|
|
curr_layer = event.layer
|
|
prev_layer = event.layer
|
|
next_layer = event.layer
|
|
|
|
prev_layer = events.events[index-1].layer if index > 0
|
|
next_layer = events.events[index+1].layer if index < len-2
|
|
|
|
if (prev_layer!=curr_layer) then
|
|
files.files[event.filename].layer_swaps += 1
|
|
end
|
|
|
|
if (next_layer!=curr_layer)
|
|
files.files[event.filename].layer_swaps += 1
|
|
end
|
|
else
|
|
filename = event.filename
|
|
size = 0
|
|
layer = event.layer
|
|
locked = event.locked
|
|
times_read = 1
|
|
total_seek = event.seek
|
|
total_read = event.read
|
|
total_blocks = event.blocks
|
|
total_actual = event.actual
|
|
|
|
files.files[event.filename] = DiskFile.new(filename,size,layer,layer_swaps,locked,times_read,total_seek,total_read,total_blocks,total_actual)
|
|
end
|
|
end
|
|
end
|
|
|
|
#
|
|
# == Description
|
|
def DiskTools::GetWorstLayerSwappingFile(events, files)
|
|
worst_layer_swaps = 0
|
|
worst_file_key = 0
|
|
files.files.each do |key, file|
|
|
if (file.layer_swaps > worst_layer_swaps) then
|
|
worst_layer_swaps = file.layer_swaps
|
|
worst_file_key = key
|
|
end
|
|
end
|
|
files.files[worst_file_key]
|
|
end
|
|
|
|
#
|
|
# == Description
|
|
def DiskTools::FlipFileLayer(events, files, file)
|
|
return if file.locked == 1
|
|
|
|
events.events.each_with_index do |event, index|
|
|
if (event.filename == file.filename) then
|
|
if event.layer == 0 then
|
|
event.layer = 1
|
|
else
|
|
event.layer = 0
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def DiskTools::Proposal(stats,new_stats,file)
|
|
response = CLICKED_NO
|
|
saving = stats.total_layer_flips - new_stats.total_layer_flips
|
|
|
|
if (saving>0) then
|
|
str = "Flipping file #{file.filename} on layer #{file.layer} would save #{saving} layer swaps (#{saving*75}ms)"
|
|
puts str
|
|
response = message_box( "#{str}","Flip Layer?", BUTTONS_YESNO, ICON_QUESTION)
|
|
else
|
|
message_box( "No point in flipping #{file.filename} Flips saved would be #{saving}","No point", BUTTONS_OK, ICON_EXCLAMATION)
|
|
end
|
|
response
|
|
end
|
|
|
|
#
|
|
# == Description
|
|
# analyse the xls file
|
|
def DiskTools::Process(worksheet, try_layer_swap, checkthruput)
|
|
|
|
puts "*************"
|
|
puts " INIT "
|
|
puts "*************\n"
|
|
disk_files = DiskFiles.new()
|
|
disk_events = DiskEvents.new()
|
|
|
|
DiskTools::ReadSheet(worksheet, disk_events)
|
|
disk_events.pretty_print
|
|
DiskTools::BuildFiles(disk_events, disk_files)
|
|
disk_files.pretty_print
|
|
|
|
orig_stats = Stats.new(disk_events, disk_files)
|
|
|
|
puts "*************"
|
|
puts " PROCESSING "
|
|
puts "*************\n"
|
|
|
|
if (try_layer_swap==1) then
|
|
disk_files.files.each do |key, file|
|
|
|
|
stats = Stats.new(disk_events, disk_files)
|
|
FlipFileLayer(disk_events, disk_files, file)
|
|
new_stats = Stats.new(disk_events, disk_files)
|
|
|
|
flip_it = Proposal(stats,new_stats,file) == CLICKED_YES
|
|
FlipFileLayer(disk_events, disk_files, file) unless flip_it
|
|
puts "#{file.filename} flipped" if flip_it
|
|
end
|
|
end
|
|
|
|
if (0) then# then checkthruput==1) then
|
|
firstTime = disk_events.events[0].time
|
|
lastTime = disk_events.events[-1].time
|
|
total_time = lastTime-firstTime
|
|
|
|
puts "Total Time : #{total_time}ms"
|
|
total_actual = 0
|
|
total_blocks = 0
|
|
total_reads = 0
|
|
total_events = 0
|
|
disk_events.events.each_with_index do |event, index|
|
|
total_actual += event.actual
|
|
total_blocks += event.blocks
|
|
total_reads += event.read
|
|
total_events += 1
|
|
end
|
|
|
|
puts "total_actual : #{total_actual}"
|
|
puts "total_blocks : #{total_blocks}"
|
|
puts "total_reads : #{total_reads}"
|
|
puts "total_events : #{total_events}"
|
|
|
|
|
|
puts "total_events/s : #{(total_events.to_f/total_time.to_f) * 1000.0}"
|
|
puts "total_blocks/s : #{(total_blocks.to_f/total_time.to_f) * 1000.0}"
|
|
data_thru = total_blocks*2
|
|
mbpers = (data_thru.to_f/total_time.to_f)/1024.0
|
|
puts "thus Mb/s : #{mbpers}"
|
|
|
|
|
|
end
|
|
|
|
puts "*************"
|
|
puts "FINAL RESULTS"
|
|
puts "*************\n"
|
|
#disk_events.pretty_print
|
|
|
|
final_disk_files = DiskFiles.new()
|
|
puts final_disk_files.class
|
|
puts final_disk_files.files.length
|
|
DiskTools::BuildFiles(disk_events, final_disk_files)
|
|
final_disk_files.pretty_print
|
|
|
|
orig_stats.pretty_print("Original stats")
|
|
puts "=>"
|
|
new_stats = Stats.new(disk_events, final_disk_files)
|
|
new_stats.pretty_print("Final Stats")
|
|
|
|
end
|
|
|
|
#
|
|
# == Description
|
|
# analyse the xls file
|
|
def DiskTools::analyse_file(xls_filename, try_layer_swap, checkthruput)
|
|
|
|
begin
|
|
puts "File not found #{xls_filename}" and return '' if not File.exists?(xls_filename)
|
|
|
|
if File::exists?( xls_filename ) then
|
|
begin
|
|
puts "Opening #{xls_filename}"
|
|
|
|
puts "Staring Excel"
|
|
begin
|
|
excel = WIN32OLE.connect("excel.application")
|
|
rescue
|
|
puts " * A new instance of Excel has been started, if you see this message more than once it may be a leaking process! * "
|
|
excel = WIN32OLE.new("excel.application") if excel==nil
|
|
end
|
|
|
|
excel.DisplayAlerts = true
|
|
|
|
WIN32OLE.const_load(excel, ExcelConst) if not ExcelConst::filled
|
|
ExcelConst::filled = true
|
|
#WIN32OLE.const_load('Microsoft Office 9.0 Object Library', MSOConst) if not MSOConst::filled
|
|
#MSOConst::filled = true
|
|
#excel.visible = true # in case you want to see what happens
|
|
puts "#{xls_filename}"
|
|
workbook = excel.Workbooks.Open(xls_filename); #OpenXML(xls_filename,nil,ExcelConst::XlXmlLoadImportToList)
|
|
worksheet = workbook.Worksheets(1)
|
|
worksheet.Activate
|
|
|
|
Process(worksheet, try_layer_swap, checkthruput)
|
|
|
|
#workbook.saveas(xls_filename) if save_xls
|
|
workbook.close
|
|
|
|
rescue Exception => ex
|
|
puts "WIN32OLE Excel stuff has quit unexpectedly:"
|
|
puts "\t#{ex.message}"
|
|
ex.backtrace.each do |m| puts "\t#{m}"; end
|
|
excel.Quit()
|
|
ensure
|
|
excel.Quit() unless excel.nil?
|
|
end # end begin 1
|
|
|
|
excel.Quit() unless excel.nil?
|
|
end # end if file exists
|
|
|
|
rescue Exception => ex
|
|
puts "DiskTools::analyse_file has quit unexpectedly as the file doesnt exist"
|
|
puts "\t#{ex.message}"
|
|
ex.backtrace.each do |m| puts "\t#{m}"; end
|
|
end
|
|
|
|
puts "File Analysed"
|
|
end # analyse_file
|
|
|
|
|
|
#-----------------------------------------------------------------------------
|
|
# Code
|
|
#-----------------------------------------------------------------------------
|
|
begin
|
|
#-------------------------------------------------------------------------
|
|
# Entry-Point
|
|
#-------------------------------------------------------------------------
|
|
|
|
g_AppName = File::basename( __FILE__, '.rb' )
|
|
g_xls_filename = ''
|
|
g_log = Log.new( 'disktools' )
|
|
|
|
#-------------------------------------------------------------------------
|
|
# Parse Command Line
|
|
#-------------------------------------------------------------------------
|
|
opts, trailing = OS::Getopt.getopts( OPTIONS )
|
|
if ( opts['help'] ) then
|
|
puts OS::Getopt.usage( OPTIONS )
|
|
response = message_box( "#{g_AppName} will exit.",g_AppName, BUTTONS_OK, ICON_QUESTION)
|
|
exit( 1 )
|
|
end
|
|
|
|
g_xls_filename = ( nil == opts['xlsfilename'] ) ? '' : opts['xlsfilename']
|
|
g_try_layer_swap = ( nil == opts['trylayerswap'] ) ? '' : opts['trylayerswap']
|
|
g_check_thruput = ( nil == opts['checkthruput'] ) ? '' : opts['checkthruput']
|
|
|
|
response = message_box( "Ready to load #{g_xls_filename}?",g_AppName, BUTTONS_OK, ICON_QUESTION)
|
|
#exit( 0 ) if response != BUTTONS_OK
|
|
|
|
puts "analyse_file #{g_xls_filename}"
|
|
DiskTools::analyse_file(g_xls_filename, g_try_layer_swap, g_check_thruput)
|
|
|
|
rescue Exception => ex
|
|
|
|
puts "#{g_AppName} unhandled exception: #{ex.message}"
|
|
puts "Call stack:"
|
|
puts ex.backtrace.join( "\n\t" )
|
|
end
|
|
|
|
end # Disktools module
|
|
|
|
# Disktools.rb |