1 /* 2 * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package jdk.jfr.internal.tool; 27 28 import java.io.BufferedInputStream; 29 import java.io.DataInputStream; 30 import java.io.File; 31 import java.io.FileInputStream; 32 import java.io.FileOutputStream; 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.io.PrintStream; 36 import java.nio.file.Files; 37 import java.nio.file.InvalidPathException; 38 import java.nio.file.Path; 39 import java.util.ArrayList; 40 import java.util.Deque; 41 import java.util.List; 42 43 import jdk.jfr.internal.consumer.ChunkHeader; 44 import jdk.jfr.internal.consumer.RecordingInput; 45 46 final class Disassemble extends Command { 47 48 @Override getName()49 public String getName() { 50 return "disassemble"; 51 } 52 53 @Override getOptionSyntax()54 public List<String> getOptionSyntax() { 55 List<String> list = new ArrayList<>(); 56 list.add("[--output <directory>]"); 57 list.add("[--max-chunks <chunks>]"); 58 list.add("[--max-size <size>]"); 59 list.add("<file>"); 60 return list; 61 } 62 63 @Override displayOptionUsage(PrintStream stream)64 public void displayOptionUsage(PrintStream stream) { 65 stream.println(" --output <directory> The location to write the disassembled file,"); 66 stream.println(" by default the current directory"); 67 stream.println(""); 68 stream.println(" --max-chunks <chunks> Maximum number of chunks per disassembled file,"); 69 stream.println(" by default 5. The chunk size varies, but is "); 70 stream.println(" typically around 15 MB."); 71 stream.println(""); 72 stream.println(" --max-size <size> Maximum number of bytes per file."); 73 stream.println(""); 74 stream.println(" <file> Location of the recording file (.jfr)"); 75 } 76 77 @Override getDescription()78 public String getDescription() { 79 return "Disassamble a recording file into smaller files/chunks"; 80 } 81 82 @Override execute(Deque<String> options)83 public void execute(Deque<String> options) throws UserSyntaxException, UserDataException { 84 if (options.isEmpty()) { 85 throw new UserSyntaxException("missing file"); 86 } 87 Path file = getJFRInputFile(options); 88 int maxChunks = Integer.MAX_VALUE; 89 int maxsize = Integer.MAX_VALUE; 90 String output = System.getProperty("user.dir"); 91 int optionCount = options.size(); 92 while (optionCount > 0) { 93 if (acceptOption(options, "--output")) { 94 output = options.pop(); 95 } 96 if (acceptOption(options, "--max-size")) { 97 String value = options.pop(); 98 try { 99 maxsize = Integer.parseInt(value); 100 if (maxsize < 1) { 101 throw new UserDataException("max size must be at least 1"); 102 } 103 } catch (NumberFormatException nfe) { 104 throw new UserDataException("not a valid value for --max-size."); 105 } 106 } 107 if (acceptOption(options, "--max-chunks")) { 108 String value = options.pop(); 109 try { 110 maxChunks = Integer.parseInt(value); 111 if (maxChunks < 1) { 112 throw new UserDataException("max chunks must be at least 1."); 113 } 114 } catch (NumberFormatException nfe) { 115 throw new UserDataException("not a valid value for --max-size."); 116 } 117 } 118 if (optionCount == options.size()) { 119 // No progress made 120 throw new UserSyntaxException("unknown option " + options.peek()); 121 } 122 optionCount = options.size(); 123 } 124 Path outputPath = getDirectory(output); 125 126 println(); 127 println("Examining recording " + file + " ..."); 128 List<Long> sizes; 129 if (maxsize != Integer.MAX_VALUE && maxChunks == Integer.MAX_VALUE) { 130 try { 131 long fileSize = Files.size(file); 132 if (maxsize >=fileSize) { 133 println(); 134 println("File size (" + fileSize +") does not exceed max size (" + maxsize + ")"); 135 return; 136 } 137 } catch (IOException e) { 138 throw new UserDataException("unexpected i/o error when determining file size" + e.getMessage()); 139 } 140 } 141 if (maxsize == Integer.MAX_VALUE && maxChunks == Integer.MAX_VALUE) { 142 maxChunks = 5; 143 } 144 145 try { 146 sizes = findChunkSizes(file); 147 } catch (IOException e) { 148 throw new UserDataException("unexpected i/o error. " + e.getMessage()); 149 } 150 if (maxsize == Integer.MAX_VALUE == sizes.size() <= maxChunks) { 151 throw new UserDataException("number of chunks in recording (" + sizes.size() + ") doesn't exceed max chunks (" + maxChunks + ")"); 152 } 153 println(); 154 if (sizes.size() > 0) { 155 List<Long> combinedSizes = combineChunkSizes(sizes, maxChunks, maxsize); 156 print("File consists of " + sizes.size() + " chunks. The recording will be split into "); 157 println(combinedSizes.size() + " files"); 158 println(); 159 splitFile(outputPath, file, combinedSizes); 160 } else { 161 throw new UserDataException("no JFR chunks found in file."); 162 } 163 } 164 findChunkSizes(Path p)165 private List<Long> findChunkSizes(Path p) throws IOException { 166 try (RecordingInput input = new RecordingInput(p.toFile())) { 167 List<Long> sizes = new ArrayList<>(); 168 ChunkHeader ch = new ChunkHeader(input); 169 sizes.add(ch.getSize()); 170 while (!ch.isLastChunk()) { 171 ch = ch.nextHeader(); 172 sizes.add(ch.getSize()); 173 } 174 return sizes; 175 } 176 } 177 combineChunkSizes(List<Long> sizes, int maxChunks, long maxSize)178 private List<Long> combineChunkSizes(List<Long> sizes, int maxChunks, long maxSize) { 179 List<Long> reduced = new ArrayList<Long>(); 180 int chunks = 1; 181 long fileSize = sizes.get(0); 182 for (int i = 1; i < sizes.size(); i++) { 183 long size = sizes.get(i); 184 if (fileSize + size > maxSize) { 185 reduced.add(fileSize); 186 chunks = 1; 187 fileSize = size; 188 continue; 189 } 190 fileSize += size; 191 if (chunks == maxChunks) { 192 reduced.add(fileSize); 193 fileSize = 0; 194 chunks = 1; 195 continue; 196 } 197 chunks++; 198 } 199 if (fileSize != 0) { 200 reduced.add(fileSize); 201 } 202 return reduced; 203 } 204 splitFile(Path directory, Path file, List<Long> splitPositions)205 private void splitFile(Path directory, Path file, List<Long> splitPositions) throws UserDataException { 206 int padAmountZeros = String.valueOf(splitPositions.size() - 1).length(); 207 String fileName = file.getFileName().toString(); 208 String fileFormatter = fileName.subSequence(0, fileName.length() - 4) + "_%0" + padAmountZeros + "d.jfr"; 209 for (int i = 0; i < splitPositions.size(); i++) { 210 String formattedFilename = String.format(fileFormatter, i); 211 try { 212 Path p = directory.resolve(formattedFilename); 213 if (Files.exists(p)) { 214 throw new UserDataException("can't create disassembled file " + p + ", a file with that name already exist"); 215 } 216 } catch (InvalidPathException ipe) { 217 throw new UserDataException("can't construct path with filename" + formattedFilename); 218 } 219 } 220 221 try (DataInputStream stream = new DataInputStream(new BufferedInputStream(new FileInputStream(file.toFile())))) { 222 for (int i = 0; i < splitPositions.size(); i++) { 223 Long l = splitPositions.get(i); 224 byte[] bytes = readBytes(stream, l.intValue()); 225 String formattedFilename = String.format(fileFormatter, i); 226 Path p = directory.resolve(formattedFilename); 227 File splittedFile = p.toFile(); 228 println("Writing " + splittedFile + " ... " + bytes.length); 229 FileOutputStream fos = new FileOutputStream(splittedFile); 230 fos.write(bytes); 231 fos.close(); 232 } 233 } catch (IOException ioe) { 234 throw new UserDataException("i/o error writing file " + file); 235 } 236 } 237 readBytes(InputStream stream, int count)238 private byte[] readBytes(InputStream stream, int count) throws UserDataException, IOException { 239 byte[] data = new byte[count]; 240 int totalRead = 0; 241 while (totalRead < data.length) { 242 int read = stream.read(data, totalRead, data.length - totalRead); 243 if (read == -1) { 244 throw new UserDataException("unexpected end of data"); 245 } 246 totalRead += read; 247 } 248 return data; 249 } 250 } 251