From 2e858dfd70e2798b7959932d01b7a47f62dfda69 Mon Sep 17 00:00:00 2001 From: Jean Luca Bez Date: Wed, 2 Feb 2022 09:27:07 -0800 Subject: [PATCH 1/5] Rename file, add option to list files, and remove the need to explicitly call Python3 --- explore.py => dxt-explorer | 70 +++++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 8 deletions(-) rename explore.py => dxt-explorer (87%) mode change 100644 => 100755 diff --git a/explore.py b/dxt-explorer old mode 100644 new mode 100755 similarity index 87% rename from explore.py rename to dxt-explorer index 56ada2b..e9f7a3c --- a/explore.py +++ b/dxt-explorer @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 """ DXT Explorer. @@ -39,16 +39,20 @@ class Explorer: def __init__(self, args): """Initialize the explorer.""" + self.args = args + self.configure_log() self.has_dxt_parser() self.has_r_support() - self.args = args - def configure_log(self): """Configure the logging system.""" self.logger = logging.getLogger('DXT Explorer') - self.logger.setLevel(logging.DEBUG) + + if self.args.debug: + self.logger.setLevel(logging.DEBUG) + else: + self.logger.setLevel(logging.INFO) # Defines the format of the logger formatter = logging.Formatter('%(asctime)s %(module)s - %(levelname)s - %(message)s') @@ -61,8 +65,11 @@ def configure_log(self): def run(self): self.is_darshan_file(self.args.darshan) - self.parse(self.args.darshan) + + if self.args.list_files: + self.list_files(self.args.darshan) + self.generate_plot(self.args.darshan) if self.args.transfer: @@ -139,7 +146,8 @@ def parse(self, file): 'offset', 'size', 'start', - 'end' + 'end', + 'file_name' ]) for line in lines: @@ -168,7 +176,8 @@ def parse(self, file): offset, size, start, - end + end, + file_name ]) if 'X_MPIIO' in line: @@ -201,9 +210,25 @@ def parse(self, file): offset, size, start, - end + end, + file_name ]) + def list_files(self, file): + files = [] + + with open(file + '.dxt.csv', 'r', newline='') as csvfile: + r = csv.DictReader(csvfile) + + for row in r: + if row['file_name'] not in files: + files.append(row['file_name']) + + + for file in files: + self.logger.info('FILE: {}'.format(file)) + + exit() def generate_plot(self, file): """Generate an interactive operation plot.""" @@ -317,6 +342,35 @@ def generate_spatiality_plot(self, file): help='Generate an interactive spatiality explorer' ) +PARSER.add_argument( + '-d', + '--debug', + action='store_true', + dest='debug', + help='Enable debug mode' +) + +PARSER.add_argument( + '-l', + '--list', + action='store_true', + dest='list_files', + help='List all the files with trace' +) + +PARSER.add_argument( + '--start', + action='store', + dest='start', + help='Report starts from X seconds from begining of the job' +) + +PARSER.add_argument( + '--finish', + action='store', + dest='finish', + help='Report finishes at X seconds from begining of the job' +) ARGS = PARSER.parse_args() From 4508b75410a8e4e32b4e5e94b2a815632392707f Mon Sep 17 00:00:00 2001 From: Jean Luca Bez Date: Wed, 23 Feb 2022 23:00:36 -0800 Subject: [PATCH 2/5] Improve generated view and include new features --- dxt-explorer | 168 +++++++++--- plots/operation.R | 669 ++++++++++++++++++++++++++++++++++++---------- plots/transfer.R | 169 ------------ 3 files changed, 667 insertions(+), 339 deletions(-) delete mode 100755 plots/transfer.R diff --git a/dxt-explorer b/dxt-explorer index e9f7a3c..847f4ee 100755 --- a/dxt-explorer +++ b/dxt-explorer @@ -26,6 +26,7 @@ import csv import shlex import argparse import subprocess +import webbrowser import logging import logging.handlers @@ -70,6 +71,8 @@ class Explorer: if self.args.list_files: self.list_files(self.args.darshan) + exit() + self.generate_plot(self.args.darshan) if self.args.transfer: @@ -94,7 +97,7 @@ class Explorer: def has_dxt_parser(self): """Check if `darshan-dxt-parser` is on PATH.""" if find_executable('darshan-dxt-parser') is not None: - self.logger.info('darshan-dxt-parser: FOUND') + self.logger.debug('darshan-dxt-parser: FOUND') else: self.logger.error('darshan-dxt-parser: NOT FOUND') @@ -103,7 +106,7 @@ class Explorer: def has_r_support(self): """Check if `Rscript` is on PATH.""" if find_executable('Rscript') is not None: - self.logger.info('Rscript: FOUND') + self.logger.debug('Rscript: FOUND') else: self.logger.error('Rscript: NOT FOUND') @@ -111,11 +114,16 @@ class Explorer: def dxt(self, file): """Parse the Darshan file to generate the .dxt trace file.""" + if os.path.exists(file + '.dxt'): + self.logger.debug('using existing parsed Darshan file') + + return + command = 'darshan-dxt-parser {0}'.format(file) args = shlex.split(command) - self.logger.info('parsing {} file'.format(file)) + self.logger.debug('parsing {} file'.format(file)) with open('{}.dxt'.format(file), 'w') as output: s = subprocess.run(args, stderr=subprocess.PIPE, stdout=output) @@ -127,7 +135,12 @@ class Explorer: """Parse the .darshan.dxt file to generate a CSV file.""" self.dxt(file) - self.logger.info('generating an intermediate CSV file') + if os.path.exists(file + '.dxt.csv'): + self.logger.debug('using existing intermediate CSV file') + + return + + self.logger.debug('generating an intermediate CSV file') with open(file + '.dxt') as f: lines = f.readlines() @@ -147,7 +160,7 @@ class Explorer: 'size', 'start', 'end', - 'file_name' + 'ost' ]) for line in lines: @@ -167,6 +180,11 @@ class Explorer: start = info[6] end = info[7] + if len(info) == 9: + ost = info[8] + else: + ost = None + w.writerow([ file_id, api.replace('X_', ''), @@ -177,7 +195,7 @@ class Explorer: size, start, end, - file_name + ost ]) if 'X_MPIIO' in line: @@ -201,6 +219,11 @@ class Explorer: start = info[5] end = info[6] + if len(info) == 9: + ost = info[8] + else: + ost = None + w.writerow([ file_id, api.replace('X_', ''), @@ -211,49 +234,106 @@ class Explorer: size, start, end, - file_name + ost ]) def list_files(self, file): files = [] + files_id = [] - with open(file + '.dxt.csv', 'r', newline='') as csvfile: - r = csv.DictReader(csvfile) + with open(file + '.dxt') as f: + lines = f.readlines() - for row in r: - if row['file_name'] not in files: - files.append(row['file_name']) + for line in lines: + if 'file_id' in line: + file_id = line.split(',')[1].split(':')[1].strip() + file_name = line.split(',')[2].split(':')[1].strip() + if file_name not in files: + files.append(file_name) + files_id.append(file_id) for file in files: self.logger.info('FILE: {}'.format(file)) + self.logger.info('{} I/O trace observation records'.format(len(lines))) - exit() + return files_id + + def subset_dataset(self, file, file_ids): + for file_id in file_ids: + subset_dataset_file = '{}.{}'.format(file, file_id) + + with open(file + '.dxt.csv') as f: + rows = csv.DictReader(f) + + with open(subset_dataset_file + '.dxt.csv', 'w', newline='') as csvfile: + w = csv.writer(csvfile) + + w.writerow([ + 'file_id', + 'api', + 'rank', + 'operation', + 'segment', + 'offset', + 'size', + 'start', + 'end', + 'ost' + ]) + + for row in rows: + if file_id == row['file_id']: + w.writerow(row.values()) def generate_plot(self, file): """Generate an interactive operation plot.""" - command = 'plots/operation.R -f {0}.dxt.csv'.format(file) + limits = '' - args = shlex.split(command) + if self.args.start: + limits += ' -s {} '.format(self.args.start) - self.logger.info('generating interactive operation plot') + if self.args.end: + limits += ' -e {} '.format(self.args.end) - s = subprocess.run(args, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + if self.args.start_rank: + limits += ' -n {} '.format(self.args.start_rank) - if s.returncode == 0: - self.logger.info('SUCCESS') - else: - self.logger.error('Failed to generate the interactive plots (error %s)', s.returncode) + if self.args.end_rank: + limits += ' -m {} '.format(self.args.end_rank) - if s.stdout is not None: - for item in s.stdout.decode().split('\n'): - if item.strip() != '': - self.logger.debug(item) + file_ids = self.list_files(file) - if s.stderr is not None: - for item in s.stderr.decode().split('\n'): - if item.strip() != '': - self.logger.error(item) + # Generated the CSV files for each plot + self.subset_dataset(file, file_ids) + + for file_id in file_ids: + command = 'plots/operation.R -f {}.{}.dxt.csv {} -o {}.{}.operation.html'.format(file, file_id, limits, file, file_id) + + args = shlex.split(command) + + self.logger.info('generating interactive operation plot') + self.logger.debug(command) + + s = subprocess.run(args, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + + if s.returncode == 0: + self.logger.info('SUCCESS') + + if self.args.browser: + webbrowser.open('file://{}.{}.operation.html'.format(file, file_id), new=2) + else: + self.logger.error('Failed to generate the interactive plots (error %s)', s.returncode) + + if s.stdout is not None: + for item in s.stdout.decode().split('\n'): + if item.strip() != '': + self.logger.debug(item) + + if s.stderr is not None: + for item in s.stderr.decode().split('\n'): + if item.strip() != '': + self.logger.error(item) def generate_transfer_plot(self, file): @@ -362,14 +442,36 @@ PARSER.add_argument( '--start', action='store', dest='start', - help='Report starts from X seconds from begining of the job' + help='Report starts from X seconds (e.g., 3.7) from beginning of the job' ) PARSER.add_argument( - '--finish', + '--end', action='store', - dest='finish', - help='Report finishes at X seconds from begining of the job' + dest='end', + help='Report ends at X seconds (e.g., 3.9) from beginning of the job' +) + +PARSER.add_argument( + '--from', + action='store', + dest='start_rank', + help='Report start from rank N' +) + +PARSER.add_argument( + '--to', + action='store', + dest='end_rank', + help='Report up to rank M' +) + +PARSER.add_argument( + '--browser', + default=False, + action='store_true', + dest='browser', + help='Open the browser with the generated plot' ) ARGS = PARSER.parse_args() diff --git a/plots/operation.R b/plots/operation.R index c012d0f..88fa286 100755 --- a/plots/operation.R +++ b/plots/operation.R @@ -17,11 +17,12 @@ # works, and perform publicly and display publicly, and to permit others to do so. packages <- c( - 'ggplot2', - 'optparse', - 'plyr', - 'plotly', - 'htmlwidgets' + 'png', + 'ggplot2', + 'optparse', + 'plyr', + 'plotly', + 'htmlwidgets' ) # Install packages not yet installed @@ -30,22 +31,57 @@ installed_packages <- packages %in% rownames(installed.packages()) dir.create(path = Sys.getenv("R_LIBS_USER"), showWarnings = FALSE, recursive = TRUE) if (any(installed_packages == FALSE)) { - install.packages(packages[!installed_packages], repos='http://cran.us.r-project.org', lib=Sys.getenv("R_LIBS_USER")) + install.packages(packages[!installed_packages], repos='http://cran.us.r-project.org', lib=Sys.getenv("R_LIBS_USER")) } # Packages loading -invisible(lapply(packages, library, character.only = TRUE)) +invisible(lapply(packages, library, warn.conflicts = FALSE, quietly = TRUE, character.only = TRUE)) option_list = list( - make_option( - c('-f', '--file'), - type = 'character', - default = NULL, + make_option( + c('-f', '--file'), + type = 'character', + default = NULL, help = 'DXT CSV file name', - metavar = 'character' + metavar = 'file' + ), + make_option( + c('-s', '--start'), + type = 'numeric', + default = NULL, + help = 'Mark trace start time', + metavar = 'start' + ), + make_option( + c('-e', '--end'), + type = 'numeric', + default = NULL, + help = 'Mark trace end time', + metavar = 'end' + ), + make_option( + c('-n', '--from'), + type = 'numeric', + default = NULL, + help = 'Display trace from rank N', + metavar = 'from' + ), + make_option( + c('-m', '--to'), + type = 'numeric', + default = NULL, + help = 'Display trace up to rank M', + metavar = 'to' + ), + make_option( + c('-o', '--output'), + type = 'character', + default = NULL, + help = 'Name of the output file', + metavar = 'output' ) ) - + opt_parser = OptionParser(option_list=option_list) opt = parse_args(opt_parser) @@ -53,147 +89,506 @@ df <- read.csv(file=opt$file, sep = ',') df$duration = df$end - df$start +duration = max(df$end) - min(df$start) + +minimum = 0 +maximum = max(df$end) + +maximum_rank = max(df$rank) + +minimum_limit = min(df$start) - (duration * 0.05) +maximum_limit = max(df$end) + (duration * 0.05) + +if (!is.null(opt$start)) { + df <- df[df$start >= opt$start, ] +} + +if (!is.null(opt$end)) { + df <- df[df$end <= opt$end, ] +} + +if (!is.null(opt$from)) { + df <- df[df$rank >= opt$from, ] +} + +if (!is.null(opt$to)) { + df <- df[df$rank <= opt$to, ] +} + df$label = paste0( - 'Rank: ', df$rank, '\n', - 'Operation: ', df$operation, '\n', - 'Duration: ', round(df$duration, digits = 3), ' seconds\n', - 'Size: ', (df$size / 1024), ' KB\n', - 'Offset: ', df$offset + 'Rank: ', df$rank, '\n', + 'Operation: ', df$operation, '\n', + 'Duration: ', round(df$duration, digits = 3), ' seconds\n', + 'Size: ', (df$size / 1024), ' KB\n', + 'Offset: ', df$offset, '\n', + 'Lustre OST: ', ifelse(is.na(df$ost), '-', df$ost) ) # Include a zero record to ensure we can facet the plot df <- rbind(df, - data.frame( - file_id = c(0, 0, 0, 0), - api = c('POSIX', 'POSIX', 'MPIIO', 'MPIIO'), - rank = c(0, 0, 0, 0), - operation = c('write', 'read', 'write', 'read'), - segment = c(0, 0, 0, 0), - offset = c(0, 0, 0, 0), - size = c(0, 0, 0, 0), - start = c(0, 0, 0, 0), - end = c(0, 0, 0, 0), - duration = c(0, 0), - label = c('', '') - ) + data.frame( + file_id = c(0, 0, 0, 0), + api = c('POSIX', 'POSIX', 'MPIIO', 'MPIIO'), + rank = c(0, 0, 0, 0), + operation = c('write', 'read', 'write', 'read'), + segment = c(0, 0, 0, 0), + offset = c(0, 0, 0, 0), + size = c(0, 0, 0, 0), + start = c(0, 0, 0, 0), + end = c(0, 0, 0, 0), + duration = c(0, 0, 0, 0), + label = c('', '', '', ''), + ost = c(NA, NA, NA, NA) + ) ) df$operation <- as.factor(df$operation) -maximum = max(df$end) + (max(df$end) * 0.01) - plot_posix <- ggplot( - df[df$api == 'POSIX', ], - aes( - x = start, - xend = end, - y = rank, - yend = rank, - color = operation, - text = label - )) + - geom_segment() + - scale_color_manual( - "", - values = c( - "#f0746e", - "#3c93c2" - ), - drop = FALSE - ) + - scale_x_continuous(breaks = seq(0, maximum, length.out = 10)) + - facet_grid(api ~ .) + - expand_limits(x = 0) + - ylim(0, max(df$rank)) + - xlab('Time') + - ylab('Rank #') + - theme_bw() + - theme( - legend.position = "top", - plot.title = element_text(size = 10), - strip.background = element_rect(colour = NA, fill = NA) - ) + df[df$api == 'POSIX', ], + aes( + xmin = start, + xmax = end, + y = rank, + color = operation, + text = label + )) + + geom_errorbarh(height=0) + +# geom_segment() + + scale_color_manual( + "", + values = c( + "#f0746e", + "#3c93c2" + ), + drop = FALSE + ) + + scale_x_continuous(limits = c(minimum_limit, maximum_limit), breaks = seq(minimum_limit, maximum_limit, length.out = 10)) + + facet_grid(api ~ ., scales="free_x") + + ylim(0, maximum_rank) + +if (!is.null(opt$start)) { + plot_posix <- plot_posix + + geom_vline( + xintercept = opt$start, + linetype = "longdash" + ) + + geom_vline( + xintercept = minimum_limit - (duration * 0.05), + alpha = 0 + ) + + annotate( + "rect", + fill = "gray", + alpha = 0.5, + xmin = minimum, + xmax = opt$start, + ymin = 0, + ymax = maximum_rank + ) +} + + plot_posix <- plot_posix + + geom_vline( + xintercept = minimum + ) + + geom_vline( + xintercept = minimum_limit, + alpha = 0 + ) + +if (!is.null(opt$end)) { + plot_posix <- plot_posix + + geom_vline( + xintercept = opt$end, + linetype = "longdash" + ) + + geom_vline( + xintercept = maximum_limit + (duration * 0.05), + alpha = 0 + ) + + annotate( + "rect", + fill = "gray", + alpha = 0.5, + xmin = opt$end, + xmax = maximum, + ymin = 0, + ymax = maximum_rank + ) +} + + plot_posix <- plot_posix + + geom_vline( + xintercept = maximum + ) + + geom_vline( + xintercept = maximum_limit, + alpha = 0 + ) + +if (!is.null(opt$from)) { + plot_posix <- plot_posix + + geom_hline( + yintercept = opt$from, + linetype = "longdash" + ) + + annotate( + "rect", + fill = "gray", + alpha = 0.5, + xmin = minimum, + xmax = maximum, + ymin = 0, + ymax = opt$from + ) +} + +if (!is.null(opt$to)) { + plot_posix <- plot_posix + + geom_hline( + yintercept = opt$to, + linetype = "longdash" + ) + + annotate( + "rect", + fill = "gray", + alpha = 0.5, + xmin = minimum, + xmax = maximum, + ymin = opt$to, + ymax = maximum_rank + ) +} + +plot_posix <- plot_posix + + xlab('Time') + + ylab('Rank #') + + theme_bw() + + theme( + legend.position = "top", + plot.title = element_text(size = 10), + strip.background = element_rect(colour = NA, fill = NA) + ) plot_mpiio <- ggplot( - df[df$api == 'MPIIO', ], - aes( - x = start, - xend = end, - y = rank, - yend = rank, - color = operation, - text = label - )) + - geom_segment() + - scale_color_manual( - "", - values = c( - "#f0746e", - "#3c93c2" - ), - drop = FALSE - ) + - scale_x_continuous(breaks = seq(0, maximum, length.out = 10)) + - facet_grid(api ~ .) + - expand_limits(x = 0) + - ylim(0, max(df$rank)) + - xlab('Time') + - ylab('Rank #') + - theme_bw() + - theme( - legend.position = "top", - plot.title = element_text(size = 10), - strip.background = element_rect(colour = NA, fill = NA) - ) + df[df$api == 'MPIIO', ], + aes( + xmin = start, + xmax = end, + y = rank, + color = operation, + text = label + )) + + geom_errorbarh(height=0) + +# geom_segment() + + scale_color_manual( + "", + values = c( + "#f0746e", + "#3c93c2" + ), + drop = FALSE + ) + + scale_x_continuous(limits = c(minimum_limit, maximum_limit), breaks = seq(minimum_limit, maximum_limit, length.out = 10)) + + facet_grid(api ~ ., scales="free_x") + + ylim(0, maximum_rank) + +if (!is.null(opt$start)) { + plot_mpiio <- plot_mpiio + + geom_vline( + xintercept = opt$start, + linetype = "longdash" + ) + + geom_vline( + xintercept = minimum_limit - (duration * 0.05), + alpha = 0 + )+ + annotate( + "rect", + fill = "gray", + alpha = 0.5, + xmin = minimum, + xmax = opt$start, + ymin = 0, + ymax = maximum_rank + ) +} + plot_mpiio <- plot_mpiio + + geom_vline( + xintercept = minimum + ) + + geom_vline( + xintercept = minimum_limit, + alpha = 0 + ) + +if (!is.null(opt$end)) { + plot_mpiio <- plot_mpiio + + geom_vline( + xintercept = opt$end, + linetype = "longdash" + ) + + geom_vline( + xintercept = maximum_limit + (duration * 0.05), + alpha = 0 + ) + + annotate( + "rect", + fill = "gray", + alpha = 0.5, + xmin = opt$end, + xmax = maximum, + ymin = 0, + ymax = maximum_rank + ) +} + plot_mpiio <- plot_mpiio + + geom_vline( + xintercept = maximum + ) + + geom_vline( + xintercept = maximum_limit, + alpha = 0 + ) + +if (!is.null(opt$from)) { + plot_mpiio <- plot_mpiio + + geom_hline( + yintercept = opt$from, + linetype = "longdash" + ) + + annotate( + "rect", + fill = "gray", + alpha = 0.5, + xmin = minimum, + xmax = maximum, + ymin = 0, + ymax = opt$from + ) +} + +if (!is.null(opt$to)) { + plot_mpiio <- plot_mpiio + + geom_hline( + yintercept = opt$to, + linetype = "longdash" + ) + + annotate( + "rect", + fill = "gray", + alpha = 0.5, + xmin = minimum, + xmax = maximum, + ymin = opt$to, + ymax = maximum_rank + ) +} + +plot_mpiio <- plot_mpiio + + xlab('Time') + + ylab('Rank #') + + theme_bw() + + theme( + legend.position = "top", + plot.title = element_text(size = 10), + strip.background = element_rect(colour = NA, fill = NA) + ) p_posix <- ggplotly( - plot_posix, - width = 1800, - height = 1000, - tooltip = "text", - legendgroup = operation, - dynamicTicks = TRUE - ) %>% - rangeslider(min(df$start), max(df$end), thickness = 0.03) %>% - layout( - margin = list(pad = 0), - legend = list(orientation = "h", x = 0, y = length(df$ranks) + 6), - autosize = TRUE, - xaxis = list(title = 'Runtime (seconds)', matches = 'x'), - yaxis = list(title = 'Rank', matches = 'y', fixedrange = FALSE), - hoverlabel = list(font = list(color = 'white')), - title = 'DXT Explorer Operation' - ) %>% - style( - showlegend = FALSE - ) %>% - toWebGL() + plot_posix, + width = 1800, + height = 1000, + tooltip = "text", + dynamicTicks = TRUE + ) + +if (!is.null(opt$start)) { + p_posix <- p_posix %>% add_annotations( + x = opt$start, + y = maximum_rank / 2, + ax = -20, + ay = 0, + text = "TIMELINE IS TRUNCATED", + textangle = -90, + font = list( + color = '#454545', + size = 10 + ) + ) +} + +if (!is.null(opt$end)) { + p_posix <- p_posix %>% add_annotations( + x = opt$end, + y = maximum_rank / 2, + ax = 20, + ay = 0, + text = "TIMELINE IS TRUNCATED", + textangle = 90, + font = list( + color = '#454545', + size = 10 + ) + ) +} + +if (!is.null(opt$from)) { + p_posix <- p_posix %>% add_annotations( + x = maximum / 2, + y = opt$from, + ax = 0, + ay = 20, + text = "RANK BEHAVIOR IS TRUNCATED", + font = list( + color = '#454545', + size = 10 + ) + ) +} + +if (!is.null(opt$to)) { + p_posix <- p_posix %>% add_annotations( + x = maximum / 2, + y = opt$to, + ax = 0, + ay = -20, + text = "RANK BEHAVIOR IS TRUNCATED", + font = list( + color = '#454545', + size = 10 + ) + ) +} + +p_posix <- p_posix %>% + rangeslider(minimum, maximum, thickness = 0.03) %>% + layout( + margin = list(pad = 0), + legend = list(orientation = "h", x = 1, y = length(df$ranks) + 6), + autosize = TRUE, + xaxis = list(title = 'Runtime (seconds)', matches = 'x'), + yaxis = list(title = 'Rank', matches = 'y', fixedrange = FALSE), + hoverlabel = list(font = list(color = 'white')), + title = paste0( + 'Explore Operation', + '
', + '', + basename(opt$file), + '' + ) + ) %>% + style( + showlegend = FALSE + ) %>% + toWebGL() p_mpiio <- ggplotly( - plot_mpiio, - width = 1800, - height = 1000, - tooltip = "text", - legendgroup = operation, - dynamicTicks = TRUE - ) %>% - layout( - margin = list(pad = 0), - legend = list(orientation = "h", x = 0, y = length(df$ranks) + 6), - autosize = TRUE, - xaxis = list(matches = 'x'), - yaxis = list(title = 'Rank', matches = 'y', fixedrange = FALSE), - hoverlabel = list(font = list(color = 'white')) - ) %>% - toWebGL() + plot_mpiio, + width = 1800, + height = 1000, + tooltip = "text", + legendgroup = operation, + dynamicTicks = TRUE + ) + +if (!is.null(opt$start)) { + p_mpiio <- p_mpiio %>% add_annotations( + x = opt$start, + y = maximum_rank / 2, + ax = -20, + ay = 0, + text = "TIMELINE IS TRUNCATED", + textangle = -90, + font = list( + color = '#454545', + size = 10 + ) + ) +} + +if (!is.null(opt$end)) { + p_mpiio <- p_mpiio %>% add_annotations( + x = opt$end, + y = maximum_rank / 2, + ax = 20, + ay = 0, + text = "TIMELINE IS TRUNCATED", + textangle = 90, + font = list( + color = '#454545', + size = 10 + ) + ) +} + +if (!is.null(opt$from)) { + p_mpiio <- p_mpiio %>% add_annotations( + x = maximum / 2, + y = opt$from, + ax = 0, + ay = 20, + text = "RANK BEHAVIOR IS TRUNCATED", + font = list( + color = '#454545', + size = 10 + ) + ) +} + +if (!is.null(opt$to)) { + p_mpiio <- p_mpiio %>% add_annotations( + x = maximum / 2, + y = opt$to, + ax = 0, + ay = -20, + text = "RANK BEHAVIOR IS TRUNCATED", + font = list( + color = '#454545', + size = 10 + ) + ) +} + +p_mpiio <- p_mpiio %>% + layout( + margin = list(pad = 0), + # legend = list(orientation = "h", x = 0, y = length(df$ranks) + 6), + autosize = TRUE, + xaxis = list(matches = 'x'), + yaxis = list(title = 'Rank', matches = 'y', fixedrange = FALSE), + hoverlabel = list(font = list(color = 'white')) + ) %>% + style( + showlegend = FALSE + ) %>% + toWebGL() p <- subplot( - p_mpiio, p_posix, - nrows = 2, - titleY = TRUE, - titleX = TRUE, - shareX = TRUE, - shareY = TRUE + p_mpiio, p_posix, + nrows = 2, + titleY = TRUE, + titleX = TRUE, + shareX = TRUE, + shareY = TRUE +) %>% +layout ( + margin = list(t = 130), + images = list( + source = raster2uri(as.raster(readPNG('dxt-explorer.png'))), + x = 0, + y = 1.02, + sizex = 0.2, + sizey = 0.2, + xref = 'paper', + yref = 'paper', + xanchor = 'middle', + yanchor = 'bottom' + ) +) %>% +config( + displaylogo = FALSE ) -saveWidget(p, selfcontained = TRUE, 'explore.html') +saveWidget(p, selfcontained = TRUE, opt$output) diff --git a/plots/transfer.R b/plots/transfer.R deleted file mode 100755 index 47cf144..0000000 --- a/plots/transfer.R +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env Rscript - -# DXT Explorer Copyright (c) 2021, The Regents of the University of -# California, through Lawrence Berkeley National Laboratory (subject -# to receipt of any required approvals from the U.S. Dept. of Energy). -# All rights reserved. -# -# If you have questions about your rights to use or distribute this software, -# please contact Berkeley Lab's Intellectual Property Office at -# IPO@lbl.gov. -# -# NOTICE. This Software was developed under funding from the U.S. Department -# of Energy and the U.S. Government consequently retains certain rights. As -# such, the U.S. Government has been granted for itself and others acting on -# its behalf a paid-up, nonexclusive, irrevocable, worldwide license in the -# Software to reproduce, distribute copies to the public, prepare derivative -# works, and perform publicly and display publicly, and to permit others to do so. - -packages <- c( - 'ggplot2', - 'optparse', - 'plyr', - 'plotly', - 'htmlwidgets', - 'wesanderson' -) - -# Install packages not yet installed -installed_packages <- packages %in% rownames(installed.packages()) - -dir.create(path = Sys.getenv("R_LIBS_USER"), showWarnings = FALSE, recursive = TRUE) - -if (any(installed_packages == FALSE)) { - install.packages(packages[!installed_packages], repos='http://cran.us.r-project.org', lib=Sys.getenv("R_LIBS_USER")) -} - -# Packages loading -invisible(lapply(packages, library, character.only = TRUE)) - -option_list = list( - make_option( - c('-f', '--file'), - type = 'character', - default = NULL, - help = 'DXT CSV file name', - metavar = 'character' - ) -) - -opt_parser = OptionParser(option_list=option_list) -opt = parse_args(opt_parser) - -df <- read.csv(file=opt$file, sep = ',') - -df$duration = df$end - df$start - -df$label = paste0('Rank: ', df$rank, '\nOperation: ', df$operation, '\nDuration: ', round(df$duration, digits = 3), ' seconds\nSize: ', (df$size / 1024), ' KB') - -df$operation <- as.factor(df$operation) - -palette <- wes_palette('Zissou1', 100, type = 'continuous') - -maximum = max(df$end) + (max(df$end) * 0.01) - -plot_posix <- ggplot( - df[df$api == 'POSIX', ], - aes( - x = start, - xend = end, - y = rank, - yend = rank, - color = size, - text = label - )) + - geom_segment() + - scale_x_continuous(breaks = seq(0, maximum, length.out = 10)) + - facet_grid(api ~ .) + - scale_color_gradientn( - 'Request size\n(bytes)', - colours = palette - ) + - expand_limits(x = 0) + - xlab('Time') + - ylab('Rank #') + - theme_bw() + - theme( - legend.position = "top", - plot.title = element_text(size = 10), - strip.background = element_rect(colour = NA, fill = NA) - ) - -plot_mpiio <- ggplot( - df[df$api == 'MPIIO', ], - aes( - x = start, - xend = end, - y = rank, - yend = rank, - color = size, - text = label - )) + - geom_segment() + - scale_x_continuous(breaks = seq(0, maximum, length.out = 10)) + - facet_grid(api ~ .) + - scale_color_gradientn( - 'Request size\n(bytes)', - colours = palette - ) + - expand_limits(x = 0) + - xlab('Time') + - ylab('Rank #') + - theme_bw() + - theme( - legend.position = "top", - plot.title = element_text(size = 10), - strip.background = element_rect(colour = NA, fill = NA) - ) - -p_posix <- ggplotly( - plot_posix, - width = 1800, - height = 1000, - tooltip = "text", - legendgroup = operation, - dynamicTicks = TRUE - ) %>% - rangeslider(min(df$start), max(df$end), thickness = 0.03) %>% - layout( - margin = list(pad = 0), - legend = list(orientation = "h", x = 0, y = length(df$ranks) + 6), - autosize = TRUE, - xaxis = list(title = 'Runtime (seconds)', matches = 'x'), - yaxis = list(title = 'Rank', matches = 'y', fixedrange = FALSE), - hoverlabel = list(font = list(color = 'white')), - title = 'DXT Explorer Transfer Size' - ) %>% - style( - showlegend = FALSE - ) %>% - toWebGL() - -p_mpiio <- ggplotly( - plot_mpiio, - width = 1800, - height = 1000, - tooltip = "text", - legendgroup = operation, - dynamicTicks = TRUE - ) %>% - layout( - margin = list(pad = 0), - legend = list(orientation = "h", x = 0, y = length(df$ranks) + 6), - autosize = TRUE, - xaxis = list(matches = 'x'), - yaxis = list(title = 'Rank', matches = 'y', fixedrange = FALSE), - hoverlabel = list(font = list(color = 'white')) - ) %>% - toWebGL() - -p <- subplot( - p_mpiio, p_posix, - nrows = 2, - titleY = TRUE, - titleX = TRUE, - shareX = TRUE, - shareY = TRUE -) - -saveWidget(p, selfcontained = TRUE, 'explore-transfer.html') From 068a20a0de3b54dab27f9c9c1be6eeea31158564 Mon Sep 17 00:00:00 2001 From: Jean Luca Bez Date: Thu, 10 Mar 2022 21:55:23 -0800 Subject: [PATCH 3/5] Include new logo and new options --- dxt-explorer | 79 ++++++++++++++++++++++++----------- logo.png | Bin 0 -> 27089 bytes logo.svg | 101 +++++++++++++++++++++++++++++++++++++++++++++ plots/operation.R | 22 +++++++++- plots/spatiality.R | 9 +++- 5 files changed, 183 insertions(+), 28 deletions(-) create mode 100644 logo.png create mode 100644 logo.svg diff --git a/dxt-explorer b/dxt-explorer index 847f4ee..5050f6d 100755 --- a/dxt-explorer +++ b/dxt-explorer @@ -253,9 +253,10 @@ class Explorer: files.append(file_name) files_id.append(file_id) - for file in files: - self.logger.info('FILE: {}'.format(file)) - self.logger.info('{} I/O trace observation records'.format(len(lines))) + for i in range(0, len(files)): + self.logger.info('FILE: {} (ID {})'.format(files[i], files_id[i])) + + self.logger.info('{} I/O trace observation records'.format(len(lines))) return files_id @@ -305,10 +306,12 @@ class Explorer: file_ids = self.list_files(file) # Generated the CSV files for each plot - self.subset_dataset(file, file_ids) + observations = self.subset_dataset(file, file_ids) for file_id in file_ids: - command = 'plots/operation.R -f {}.{}.dxt.csv {} -o {}.{}.operation.html'.format(file, file_id, limits, file, file_id) + output_file = '{}.{}.operation.html'.format(file, file_id) + + command = 'plots/operation.R -f {}.{}.dxt.csv {} -o {}'.format(file, file_id, limits, output_file) args = shlex.split(command) @@ -318,12 +321,15 @@ class Explorer: s = subprocess.run(args, stderr=subprocess.PIPE, stdout=subprocess.PIPE) if s.returncode == 0: - self.logger.info('SUCCESS') + if os.path.exists(output_file): + self.logger.info('SUCCESS') + else: + self.logger.warning('no data to generate interactive plots') if self.args.browser: - webbrowser.open('file://{}.{}.operation.html'.format(file, file_id), new=2) + webbrowser.open('file://{}'.format(output_file), new=2) else: - self.logger.error('Failed to generate the interactive plots (error %s)', s.returncode) + self.logger.error('failed to generate the interactive plots (error %s)', s.returncode) if s.stdout is not None: for item in s.stdout.decode().split('\n'): @@ -338,29 +344,52 @@ class Explorer: def generate_transfer_plot(self, file): """Generate an interactive transfer plot.""" - command = 'plots/transfer.R -f {0}.dxt.csv'.format(file) + limits = '' - args = shlex.split(command) + if self.args.start: + limits += ' -s {} '.format(self.args.start) - self.logger.info('generating interactive transfer plot') + if self.args.end: + limits += ' -e {} '.format(self.args.end) - s = subprocess.run(args, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + if self.args.start_rank: + limits += ' -n {} '.format(self.args.start_rank) - if s.returncode == 0: - self.logger.info('SUCCESS') - else: - self.logger.error('Failed to generate the interactive plots (error %s)', s.returncode) + if self.args.end_rank: + limits += ' -m {} '.format(self.args.end_rank) - if s.stdout is not None: - for item in s.stdout.decode().split('\n'): - if item.strip() != '': - self.logger.debug(item) + file_ids = self.list_files(file) - if s.stderr is not None: - for item in s.stderr.decode().split('\n'): - if item.strip() != '': - self.logger.error(item) + # Generated the CSV files for each plot + self.subset_dataset(file, file_ids) + + for file_id in file_ids: + command = 'plots/transfer.R -f {}.{}.dxt.csv {} -o {}.{}.transfer.html'.format(file, file_id, limits, file, file_id) + + args = shlex.split(command) + + self.logger.info('generating interactive transfer plot') + self.logger.debug(command) + + s = subprocess.run(args, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + + if s.returncode == 0: + self.logger.info('SUCCESS') + + if self.args.browser: + webbrowser.open('file://{}.{}.transfer.html'.format(file, file_id), new=2) + else: + self.logger.error('failed to generate the interactive plots (error %s)', s.returncode) + if s.stdout is not None: + for item in s.stdout.decode().split('\n'): + if item.strip() != '': + self.logger.debug(item) + + if s.stderr is not None: + for item in s.stderr.decode().split('\n'): + if item.strip() != '': + self.logger.error(item) def generate_spatiality_plot(self, file): """Generate an interactive spatiality plot.""" @@ -375,7 +404,7 @@ class Explorer: if s.returncode == 0: self.logger.info('SUCCESS') else: - self.logger.error('Failed to generate the interactive plots (error %s)', s.returncode) + self.logger.error('failed to generate the interactive plots (error %s)', s.returncode) if s.stdout is not None: for item in s.stdout.decode().split('\n'): diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f0079fe4564706a4cb6394018c34e7999624518b GIT binary patch literal 27089 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 F`hPj|;@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plots/operation.R b/plots/operation.R index 88fa286..0720602 100755 --- a/plots/operation.R +++ b/plots/operation.R @@ -22,6 +22,7 @@ packages <- c( 'optparse', 'plyr', 'plotly', + 'rmarkdown', 'htmlwidgets' ) @@ -37,6 +38,12 @@ if (any(installed_packages == FALSE)) { # Packages loading invisible(lapply(packages, library, warn.conflicts = FALSE, quietly = TRUE, character.only = TRUE)) +if (pandoc_available()) { + self_contained = TRUE +} else { + self_contained = FALSE +} + option_list = list( make_option( c('-f', '--file'), @@ -79,6 +86,13 @@ option_list = list( default = NULL, help = 'Name of the output file', metavar = 'output' + ), + make_option( + c('-u', '--html'), + type = 'logical', + default = TRUE, + help = 'Generate a self-contained HTML file (requires pandoc)', + metavar = 'html' ) ) @@ -96,7 +110,7 @@ maximum = max(df$end) maximum_rank = max(df$rank) -minimum_limit = min(df$start) - (duration * 0.05) +minimum_limit = -0.05 maximum_limit = max(df$end) + (duration * 0.05) if (!is.null(opt$start)) { @@ -115,6 +129,10 @@ if (!is.null(opt$to)) { df <- df[df$rank <= opt$to, ] } +if (nrow(df) == 0) { + quit() +} + df$label = paste0( 'Rank: ', df$rank, '\n', 'Operation: ', df$operation, '\n', @@ -591,4 +609,4 @@ config( displaylogo = FALSE ) -saveWidget(p, selfcontained = TRUE, opt$output) +saveWidget(p, selfcontained = self_contained, opt$output) diff --git a/plots/spatiality.R b/plots/spatiality.R index 9a71b78..a3bca2b 100755 --- a/plots/spatiality.R +++ b/plots/spatiality.R @@ -21,6 +21,7 @@ packages <- c( 'optparse', 'plyr', 'plotly', + 'rmarkdown', 'htmlwidgets', 'wesanderson' ) @@ -37,6 +38,12 @@ if (any(installed_packages == FALSE)) { # Packages loading invisible(lapply(packages, library, character.only = TRUE)) +if (pandoc_available()) { + self_contained = TRUE +} else { + self_contained = FALSE +} + option_list = list( make_option( c('-f', '--file'), @@ -190,4 +197,4 @@ p <- subplot( shareY = TRUE ) -saveWidget(p, selfcontained = TRUE, 'explore-spatiality.html') +saveWidget(p, selfcontained = self_contained, 'explore-spatiality.html') From 230b59012beb39a13489488d035c659d784046eb Mon Sep 17 00:00:00 2001 From: Jean Luca Bez Date: Thu, 24 Mar 2022 10:47:21 -0700 Subject: [PATCH 4/5] Create setup.py --- dxt-explorer | 21 +++++++++------------ plots/operation.R | 9 ++++++++- setup.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 setup.py diff --git a/dxt-explorer b/dxt-explorer index 5050f6d..6b439e8 100755 --- a/dxt-explorer +++ b/dxt-explorer @@ -32,7 +32,6 @@ import logging.handlers import pandas as pd -from plotnine import * from distutils.spawn import find_executable @@ -238,8 +237,7 @@ class Explorer: ]) def list_files(self, file): - files = [] - files_id = [] + files = {} with open(file + '.dxt') as f: lines = f.readlines() @@ -249,16 +247,15 @@ class Explorer: file_id = line.split(',')[1].split(':')[1].strip() file_name = line.split(',')[2].split(':')[1].strip() - if file_name not in files: - files.append(file_name) - files_id.append(file_id) + if file_id not in files.keys(): + files[file_id] = file_name - for i in range(0, len(files)): - self.logger.info('FILE: {} (ID {})'.format(files[i], files_id[i])) + for file_id, file_name in files.items(): + self.logger.info('FILE: {} (ID {})'.format(file_name, file_id)) - self.logger.info('{} I/O trace observation records'.format(len(lines))) + self.logger.info('{} I/O trace observation records from {} files'.format(len(lines), len(files))) - return files_id + return files def subset_dataset(self, file, file_ids): for file_id in file_ids: @@ -308,10 +305,10 @@ class Explorer: # Generated the CSV files for each plot observations = self.subset_dataset(file, file_ids) - for file_id in file_ids: + for file_id, file_name in file_ids.items(): output_file = '{}.{}.operation.html'.format(file, file_id) - command = 'plots/operation.R -f {}.{}.dxt.csv {} -o {}'.format(file, file_id, limits, output_file) + command = 'plots/operation.R -f {}.{}.dxt.csv {} -o {} -x {}'.format(file, file_id, limits, output_file, file_name) args = shlex.split(command) diff --git a/plots/operation.R b/plots/operation.R index 0720602..639871e 100755 --- a/plots/operation.R +++ b/plots/operation.R @@ -93,6 +93,13 @@ option_list = list( default = TRUE, help = 'Generate a self-contained HTML file (requires pandoc)', metavar = 'html' + ), + make_option( + c('-x', '--identifier'), + type = 'character', + default = TRUE, + help = 'Set the identifier of the original file captured by Darshan DXT', + metavar = 'identifier' ) ) @@ -493,7 +500,7 @@ p_posix <- p_posix %>% 'Explore Operation', '
', '', - basename(opt$file), + opt$identifier, '' ) ) %>% diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6f08915 --- /dev/null +++ b/setup.py @@ -0,0 +1,33 @@ +import setuptools + +with open("README.md", "r") as f: + long_description = f.read() + +with open("requirements.txt") as f: + requirements = f.readlines() + +setuptools.setup( + name="dxt-explorer", + keywords="dxt-explorer", + version="1.2", + author="Jean Luca Bez, Suren Byna", + author_email="jlbez@lbl.gov, sbyna@lbl.gov", + description="DXT Explorer is an interactive web-based log analysis tool to visualize Darshan DXT logs and help understand the I/O behavior of applications.", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/hpc-io/dxt-explorer", + install_requires=requirements, + packages=setuptools.find_packages(), + scripts=[ + "dxt-explorer" + ], + classifiers=[ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: Other/Proprietary License", + "Programming Language :: Python :: 3 :: Only" + ], + python_requires='>=3.6', +) \ No newline at end of file From 6c3f5c21d7f34a8e65f58e31277a11344333d3a3 Mon Sep 17 00:00:00 2001 From: Jean Luca Bez Date: Thu, 24 Mar 2022 18:07:33 -0700 Subject: [PATCH 5/5] Update requirements.txt --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index d78ae50..67f6f40 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ argparse -pandas -plotnine +pandas \ No newline at end of file