/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.ignite.internal.visor.igfs; import java.io.BufferedReader; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.DirectoryStream; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.IgniteException; import org.apache.ignite.igfs.IgfsMode; import org.apache.ignite.internal.processors.task.GridInternal; import org.apache.ignite.internal.util.typedef.internal.S; import org.apache.ignite.internal.util.typedef.internal.U; import org.apache.ignite.internal.visor.VisorJob; import org.apache.ignite.internal.visor.VisorOneNodeTask; import static org.apache.ignite.internal.igfs.common.IgfsLogger.DELIM_FIELD; import static org.apache.ignite.internal.igfs.common.IgfsLogger.HDR; import static org.apache.ignite.internal.igfs.common.IgfsLogger.TYPE_CLOSE_IN; import static org.apache.ignite.internal.igfs.common.IgfsLogger.TYPE_CLOSE_OUT; import static org.apache.ignite.internal.igfs.common.IgfsLogger.TYPE_OPEN_IN; import static org.apache.ignite.internal.igfs.common.IgfsLogger.TYPE_OPEN_OUT; import static org.apache.ignite.internal.igfs.common.IgfsLogger.TYPE_RANDOM_READ; import static org.apache.ignite.internal.visor.util.VisorTaskUtils.resolveIgfsProfilerLogsDir; /** * Task that parse hadoop profiler logs. */ @GridInternal public class VisorIgfsProfilerTask extends VisorOneNodeTask<VisorIgfsProfilerTaskArg, List<VisorIgfsProfilerEntry>> { /** */ private static final long serialVersionUID = 0L; /** * Holder class for parsed data. */ private static class VisorIgfsProfilerParsedLine { /** Timestamp. */ private final long ts; /** Log entry type. */ private final int entryType; /** File path. */ private final String path; /** File IGFS mode. */ private final IgfsMode mode; /** Stream ID. */ private final long streamId; /** Data length. */ private final long dataLen; /** Overwrite flag. Available only for OPEN_OUT event. */ private final boolean overwrite; /** Position of data being randomly read or seek. Available only for RANDOM_READ or SEEK events. */ private final long pos; /** User time. Available only for CLOSE_IN/CLOSE_OUT events. */ private final long userTime; /** System time (either read or write). Available only for CLOSE_IN/CLOSE_OUT events. */ private final long sysTime; /** Total amount of read or written bytes. Available only for CLOSE_IN/CLOSE_OUT events. */ private final long totalBytes; /** * Create holder for log line. */ private VisorIgfsProfilerParsedLine( long ts, int entryType, String path, IgfsMode mode, long streamId, long dataLen, boolean overwrite, long pos, long userTime, long sysTime, long totalBytes ) { this.ts = ts; this.entryType = entryType; this.path = path; this.mode = mode; this.streamId = streamId; this.dataLen = dataLen; this.overwrite = overwrite; this.pos = pos; this.userTime = userTime; this.sysTime = sysTime; this.totalBytes = totalBytes; } } /** * Comparator to sort parsed log lines by timestamp. */ private static final Comparator<VisorIgfsProfilerParsedLine> PARSED_LINE_BY_TS_COMPARATOR = new Comparator<VisorIgfsProfilerParsedLine>() { @Override public int compare(VisorIgfsProfilerParsedLine a, VisorIgfsProfilerParsedLine b) { return a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0; } }; /** {@inheritDoc} */ @Override protected VisorIgfsProfilerJob job(VisorIgfsProfilerTaskArg arg) { return new VisorIgfsProfilerJob(arg, debug); } /** * Job that do actual profiler work. */ private static class VisorIgfsProfilerJob extends VisorJob<VisorIgfsProfilerTaskArg, List<VisorIgfsProfilerEntry>> { /** */ private static final long serialVersionUID = 0L; // Named column indexes in log file. /** */ private static final int LOG_COL_TIMESTAMP = 0; /** */ private static final int LOG_COL_THREAD_ID = 1; /** */ private static final int LOG_COL_ENTRY_TYPE = 3; /** */ private static final int LOG_COL_PATH = 4; /** */ private static final int LOG_COL_IGFS_MODE = 5; /** */ private static final int LOG_COL_STREAM_ID = 6; /** */ private static final int LOG_COL_DATA_LEN = 8; /** */ private static final int LOG_COL_OVERWRITE = 10; /** */ private static final int LOG_COL_POS = 13; /** */ private static final int LOG_COL_USER_TIME = 17; /** */ private static final int LOG_COL_SYSTEM_TIME = 18; /** */ private static final int LOG_COL_TOTAL_BYTES = 19; /** List of log entries that should be parsed. */ private static final Set<Integer> LOG_TYPES; static { LOG_TYPES = new HashSet<>(); LOG_TYPES.add(TYPE_OPEN_IN); LOG_TYPES.add(TYPE_OPEN_OUT); LOG_TYPES.add(TYPE_RANDOM_READ); LOG_TYPES.add(TYPE_CLOSE_IN); LOG_TYPES.add(TYPE_CLOSE_OUT); } /** * Create job with given argument. * * @param arg IGFS name. * @param debug Debug flag. */ private VisorIgfsProfilerJob(VisorIgfsProfilerTaskArg arg, boolean debug) { super(arg, debug); } /** {@inheritDoc} */ @Override protected List<VisorIgfsProfilerEntry> run(VisorIgfsProfilerTaskArg arg) { String name = arg.getIgfsName(); try { Path logsDir = resolveIgfsProfilerLogsDir(ignite.fileSystem(name)); if (logsDir != null) return parse(logsDir, name); return Collections.emptyList(); } catch (IOException | IllegalArgumentException e) { throw new IgniteException("Failed to parse profiler logs for IGFS: " + name, e); } catch (IgniteCheckedException e) { throw U.convertException(e); } } /** * Parse boolean. * * @param ss Array of source strings. * @param ix Index of array item to parse. * @return Parsed boolean. */ private boolean parseBoolean(String[] ss, int ix) { return ix < ss.length && "1".equals(ss[ix]); } /** * Parse integer. * * @param ss Array of source strings. * @param ix Index of array item to parse. * @param dflt Default value if string is empty or index is out of array bounds. * @return Parsed integer. * @throws NumberFormatException if the string does not contain a parsable integer. */ private int parseInt(String[] ss, int ix, int dflt) { if (ss.length <= ix) return dflt; else { String s = ss[ix]; return s.isEmpty() ? dflt : Integer.parseInt(s); } } /** * Parse long. * * @param ss Array of source strings. * @param ix Index of array item to parse. * @param dflt Default value if string is empty or index is out of array bounds. * @return Parsed integer. * @throws NumberFormatException if the string does not contain a parsable long. */ private long parseLong(String[] ss, int ix, long dflt) { if (ss.length <= ix) return dflt; else { String s = ss[ix]; return s.isEmpty() ? dflt : Long.parseLong(s); } } /** * Parse string. * * @param ss Array of source strings. * @param ix Index of array item to parse. * @return Parsed string. */ private String parseString(String[] ss, int ix) { if (ss.length <= ix) return ""; else { String s = ss[ix]; return s.isEmpty() ? "" : s; } } /** * Parse IGFS mode from string. * * @param ss Array of source strings. * @param ix Index of array item to parse. * @return Parsed IGFS mode or {@code null} if string is empty. */ private IgfsMode parseIgfsMode(String[] ss, int ix) { if (ss.length <= ix) return null; else { String s = ss[ix]; return s.isEmpty() ? null : IgfsMode.valueOf(s); } } /** * Parse line from log. * * @param s Line with text to parse. * @return Parsed data. */ private VisorIgfsProfilerParsedLine parseLine(String s) { String[] ss = s.split(DELIM_FIELD); long streamId = parseLong(ss, LOG_COL_STREAM_ID, -1); if (streamId >= 0) { int entryType = parseInt(ss, LOG_COL_ENTRY_TYPE, -1); // Parse only needed types. if (LOG_TYPES.contains(entryType)) return new VisorIgfsProfilerParsedLine( parseLong(ss, LOG_COL_TIMESTAMP, 0), entryType, parseString(ss, LOG_COL_PATH), parseIgfsMode(ss, LOG_COL_IGFS_MODE), streamId, parseLong(ss, LOG_COL_DATA_LEN, 0), parseBoolean(ss, LOG_COL_OVERWRITE), parseLong(ss, LOG_COL_POS, 0), parseLong(ss, LOG_COL_USER_TIME, 0), parseLong(ss, LOG_COL_SYSTEM_TIME, 0), parseLong(ss, LOG_COL_TOTAL_BYTES, 0) ); } return null; } /** * Aggregate information from parsed lines grouped by {@code streamId}. */ private VisorIgfsProfilerEntry aggregateParsedLines(List<VisorIgfsProfilerParsedLine> lines) { VisorIgfsProfilerUniformityCounters counters = new VisorIgfsProfilerUniformityCounters(); Collections.sort(lines, PARSED_LINE_BY_TS_COMPARATOR); String path = ""; long ts = 0; long size = 0; long bytesRead = 0; long readTime = 0; long userReadTime = 0; long bytesWritten = 0; long writeTime = 0; long userWriteTime = 0; IgfsMode mode = null; for (VisorIgfsProfilerParsedLine line : lines) { if (!line.path.isEmpty()) path = line.path; ts = line.ts; // Remember last timestamp. // Remember last IGFS mode. if (line.mode != null) mode = line.mode; switch (line.entryType) { case TYPE_OPEN_IN: size = line.dataLen; // Remember last file size. counters.invalidate(size); break; case TYPE_OPEN_OUT: if (line.overwrite) { size = 0; // If file was overridden, set size to zero. counters.invalidate(size); } break; case TYPE_CLOSE_IN: bytesRead += line.totalBytes; // Add to total bytes read. readTime += line.sysTime; // Add to read time. userReadTime += line.userTime; // Add to user read time. counters.increment(line.pos, line.totalBytes); break; case TYPE_CLOSE_OUT: size += line.totalBytes; // Add to files size. bytesWritten += line.totalBytes; // Add to total bytes written. writeTime += line.sysTime; // Add to write time. userWriteTime += line.userTime; // Add to user write time. counters.invalidate(size); break; case TYPE_RANDOM_READ: counters.increment(line.pos, line.totalBytes); break; default: throw new IllegalStateException("Unexpected IGFS profiler log entry type: " + line.entryType); } } // Return only fully parsed data with path. return path.isEmpty() ? null : new VisorIgfsProfilerEntry( path, ts, mode, size, bytesRead, readTime, userReadTime, bytesWritten, writeTime, userWriteTime, counters); } /** * @param p Path to log file to parse. * @return Collection of parsed and aggregated entries. * @throws IOException if failed to read log file. */ private List<VisorIgfsProfilerEntry> parseFile(Path p) throws IOException { List<VisorIgfsProfilerParsedLine> parsedLines = new ArrayList<>(512); try (BufferedReader br = Files.newBufferedReader(p, Charset.forName("UTF-8"))) { String line = br.readLine(); // Skip first line with columns header. if (line != null) { // Check file header. if (HDR.equalsIgnoreCase(line)) line = br.readLine(); while (line != null) { try { VisorIgfsProfilerParsedLine ln = parseLine(line); if (ln != null) parsedLines.add(ln); } catch (NumberFormatException ignored) { // Skip invalid lines. } line = br.readLine(); } } } // Group parsed lines by streamId. Map<Long, List<VisorIgfsProfilerParsedLine>> byStreamId = new HashMap<>(); for (VisorIgfsProfilerParsedLine line : parsedLines) { List<VisorIgfsProfilerParsedLine> grp = byStreamId.get(line.streamId); if (grp == null) { grp = new ArrayList<>(); byStreamId.put(line.streamId, grp); } grp.add(line); } // Aggregate each group. List<VisorIgfsProfilerEntry> entries = new ArrayList<>(byStreamId.size()); for (List<VisorIgfsProfilerParsedLine> lines : byStreamId.values()) { VisorIgfsProfilerEntry entry = aggregateParsedLines(lines); if (entry != null) entries.add(entry); } // Group by files. Map<String, List<VisorIgfsProfilerEntry>> byPath = new HashMap<>(); for (VisorIgfsProfilerEntry entry : entries) { List<VisorIgfsProfilerEntry> grp = byPath.get(entry.getPath()); if (grp == null) { grp = new ArrayList<>(); byPath.put(entry.getPath(), grp); } grp.add(entry); } // Aggregate by files. List<VisorIgfsProfilerEntry> res = new ArrayList<>(byPath.size()); for (List<VisorIgfsProfilerEntry> lst : byPath.values()) res.add(VisorIgfsProfiler.aggregateIgfsProfilerEntries(lst)); return res; } /** * Parse all IGFS log files in specified log directory. * * @param logDir Folder were log files located. * @return List of line with aggregated information by files. */ private List<VisorIgfsProfilerEntry> parse(Path logDir, String igfsName) throws IOException { List<VisorIgfsProfilerEntry> parsedFiles = new ArrayList<>(512); try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(logDir)) { PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:igfs-log-" + igfsName + "-*.csv"); for (Path p : dirStream) { if (matcher.matches(p.getFileName())) { try { parsedFiles.addAll(parseFile(p)); } catch (NoSuchFileException ignored) { // Files was deleted, skip it. } catch (Exception e) { ignite.log().warning("Failed to parse IGFS profiler log file: " + p, e); } } } } return parsedFiles; } /** {@inheritDoc} */ @Override public String toString() { return S.toString(VisorIgfsProfilerJob.class, this); } } }