1 /* 2 * Copyright 2019 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.perfmark.tracewriter; 18 19 import static java.nio.charset.StandardCharsets.UTF_8; 20 21 import com.google.errorprone.annotations.CanIgnoreReturnValue; 22 import com.google.gson.Gson; 23 import com.google.gson.GsonBuilder; 24 import com.google.gson.JsonIOException; 25 import com.google.gson.annotations.SerializedName; 26 import io.perfmark.impl.Mark; 27 import io.perfmark.impl.MarkList; 28 import io.perfmark.impl.Storage; 29 import java.io.File; 30 import java.io.IOException; 31 import java.io.OutputStream; 32 import java.io.OutputStreamWriter; 33 import java.io.Writer; 34 import java.lang.management.ManagementFactory; 35 import java.lang.reflect.Method; 36 import java.nio.file.Files; 37 import java.nio.file.Path; 38 import java.util.ArrayDeque; 39 import java.util.ArrayList; 40 import java.util.Collections; 41 import java.util.Deque; 42 import java.util.HashMap; 43 import java.util.LinkedHashMap; 44 import java.util.List; 45 import java.util.Map; 46 import java.util.logging.Level; 47 import java.util.logging.Logger; 48 import java.util.zip.GZIPOutputStream; 49 50 /** 51 * Writes the PerfMark results to a "Trace Event" JSON file usable by the Chromium Profiler 52 * "Catapult". The format is defined at 53 * https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview 54 * 55 * <p>This code is <strong>NOT</strong> API stable, and may be removed in the future, or changed 56 * without notice. 57 * 58 * @since 0.16.0 59 */ 60 public final class TraceEventWriter { 61 62 private static final Logger logger = Logger.getLogger(TraceEventWriter.class.getName()); 63 64 /** 65 * Writes trace events the home directory. By default, it prefers the location in {@code 66 * $XDG_DATA_HOME/perfmark} environment variable. If unset, it attempts to use {@code 67 * $HOME/.local/share/perfmark}. 68 * 69 * <p>Authors note: if you are on Windows, or the above defaults aren't right, I'm not really sure 70 * where else is a good place to put this data. Please file an issue at https://www.perfmark.io/ 71 * if you have a preference. 72 * 73 * <p>Updated in 0.17.0 to return the created path. 74 * 75 * @throws IOException if there is an error writing to the file. 76 * @return the path used to create the trace file. 77 */ 78 @CanIgnoreReturnValue writeTraceEvents()79 public static Path writeTraceEvents() throws IOException { 80 Path p = pickNextDest(guessDirectory()); 81 try (OutputStream os = Files.newOutputStream(p); 82 OutputStream gzos = new GZIPOutputStream(os); 83 Writer osw = new OutputStreamWriter(gzos, UTF_8)) { 84 writeTraceEvents(osw); 85 } 86 logger.info("Wrote trace to " + p); 87 return p; 88 } 89 90 /** 91 * Writes all trace events in in JSON format to the given destination. 92 * 93 * @param destination the destination for the JSON data. 94 * @throws IOException if there are errors build the JSON, or can't write to the destination. 95 */ writeTraceEvents(Writer destination)96 public static void writeTraceEvents(Writer destination) throws IOException { 97 writeTraceEvents( 98 destination, Storage.read(), Storage.getInitNanoTime(), System.nanoTime(), getPid()); 99 } 100 101 /** 102 * Writes the trace events gathered from {@link Storage#read()}. This method is not API stable. It 103 * will be eventually. 104 * 105 * @param destination the destination for the JSON data. 106 * @param markLists the data to use to build the trace event JSON 107 * @param initNanoTime the time PerfMark classes were first loaded as specified by {@link 108 * System#nanoTime()} 109 * @param nowNanoTime the current time as specified by {@link System#nanoTime()}. 110 * @param pid the PID of the current process. 111 * @throws IOException if there are errors build the JSON, or can't write to the destination. 112 */ writeTraceEvents( Writer destination, List<? extends MarkList> markLists, long initNanoTime, long nowNanoTime, long pid)113 public static void writeTraceEvents( 114 Writer destination, 115 List<? extends MarkList> markLists, 116 long initNanoTime, 117 long nowNanoTime, 118 long pid) 119 throws IOException { 120 List<TraceEvent> traceEvents = new ArrayList<>(); 121 new TraceEventWalker(traceEvents, pid, initNanoTime).walk(markLists, nowNanoTime); 122 Gson gson = new GsonBuilder().create(); 123 try { 124 gson.toJson(new TraceEventObject(traceEvents), destination); 125 } catch (JsonIOException e) { 126 throw new IOException(e); 127 } 128 } 129 pickNextDest(Path dir)130 private static Path pickNextDest(Path dir) throws IOException { 131 String fmt = "perfmark-trace-%03d.json.gz"; 132 int lo; 133 int hi = 0; 134 while (true) { 135 Path candidate = dir.resolve(String.format(fmt, hi)); 136 if (!Files.exists(candidate)) { 137 lo = hi >>> 1; 138 break; 139 } 140 if (hi == 0) { 141 hi++; 142 } else if (hi >>> 1 >= Integer.MAX_VALUE) { 143 throw new IOException("too many files in dir"); 144 } else { 145 hi <<= 1; 146 } 147 } 148 // After this point, hi must always point to a non-existent file. 149 while (hi > lo) { 150 int mid = (hi + lo) >>> 1; // take THAT, overflow! 151 Path candidate = dir.resolve(String.format(fmt, mid)); 152 if (Files.exists(candidate)) { 153 lo = mid + 1; 154 } else { 155 hi = mid; 156 } 157 } 158 return dir.resolve(String.format(fmt, hi)); 159 } 160 guessDirectory()161 private static Path guessDirectory() throws IOException { 162 final String PERFMARK_TRACE_DIR = "perfmark"; 163 final String sep = File.separator; 164 165 List<Path> dataHomeChoices = new ArrayList<>(); 166 String dataHome = System.getenv("XDG_DATA_HOME"); 167 if (dataHome != null) { 168 dataHomeChoices.add(new File(dataHome + sep + PERFMARK_TRACE_DIR).toPath()); 169 } 170 String home = System.getProperty("user.home"); 171 if (home != null) { 172 dataHomeChoices.add( 173 new File(home + sep + ".local" + sep + "share" + sep + PERFMARK_TRACE_DIR).toPath()); 174 } 175 for (Path path : dataHomeChoices) { 176 if (!Files.exists(path)) { 177 Files.createDirectories(path); 178 } else { 179 if (!Files.isDirectory(path)) { 180 continue; 181 } 182 } 183 return path; 184 } 185 return new File(".").toPath(); 186 } 187 188 static final class TraceEventObject { 189 @SerializedName("traceEvents") 190 @SuppressWarnings("unused") 191 final List<TraceEvent> traceEvents; 192 193 @SerializedName("displayTimeUnit") 194 @SuppressWarnings("unused") 195 final String displayTimeUnit = "ns"; 196 197 @SerializedName("systemTraceEvents") 198 @SuppressWarnings("unused") 199 final String systemTraceData = ""; 200 201 @SerializedName("samples") 202 @SuppressWarnings("unused") 203 final List<Object> samples = new ArrayList<>(); 204 205 @SerializedName("stackFrames") 206 @SuppressWarnings("unused") 207 final Map<String, ?> stackFrames = new HashMap<>(); 208 TraceEventObject(List<TraceEvent> traceEvents)209 TraceEventObject(List<TraceEvent> traceEvents) { 210 this.traceEvents = Collections.unmodifiableList(new ArrayList<>(traceEvents)); 211 } 212 } 213 getPid()214 private static long getPid() { 215 List<Throwable> errors = new ArrayList<>(0); 216 Level level = Level.FINE; 217 try { 218 try { 219 Class<?> clz = Class.forName("java.lang.ProcessHandle"); 220 Method currentMethod = clz.getMethod("current"); 221 Object processHandle = currentMethod.invoke(null); 222 Method pidMethod = clz.getMethod("pid"); 223 return (long) pidMethod.invoke(processHandle); 224 } catch (Exception | Error e) { 225 errors.add(e); 226 } 227 try { 228 String name = ManagementFactory.getRuntimeMXBean().getName(); 229 int index = name.indexOf('@'); 230 if (index != -1) { 231 return Long.parseLong(name.substring(0, index)); 232 } 233 } catch (Exception | Error e) { 234 errors.add(e); 235 } 236 level = Level.WARNING; 237 } finally { 238 for (Throwable error : errors) { 239 logger.log(level, "Error getting pid", error); 240 } 241 } 242 return -1; 243 } 244 245 private static final class TraceEventWalker extends MarkListWalker { 246 247 private static final class TaskStart { 248 final Mark mark; 249 final int traceEventIdx; 250 TaskStart(Mark mark, int traceEventIdx)251 TaskStart(Mark mark, int traceEventIdx) { 252 this.mark = mark; 253 this.traceEventIdx = traceEventIdx; 254 } 255 } 256 257 private long uniqueLinkPairId = 1; 258 private long currentThreadId = -1; 259 private long currentMarkListId = -1; 260 private final Deque<TaskStart> taskStack = new ArrayDeque<>(); 261 private final Map<Long, LinkTuple> linkIdToLinkOut = new LinkedHashMap<>(); 262 private final List<LinkTuple> linkIdToLinkIn = new ArrayList<>(); 263 264 private final long pid; 265 private final long initNanoTime; 266 private final List<TraceEvent> traceEvents; 267 TraceEventWalker(List<TraceEvent> traceEvents, long pid, long initNanoTime)268 TraceEventWalker(List<TraceEvent> traceEvents, long pid, long initNanoTime) { 269 this.pid = pid; 270 this.initNanoTime = initNanoTime; 271 this.traceEvents = traceEvents; 272 } 273 274 @Override enterGeneration(long generation)275 protected void enterGeneration(long generation) { 276 linkIdToLinkOut.clear(); 277 linkIdToLinkIn.clear(); 278 } 279 280 @Override exitGeneration()281 protected void exitGeneration() { 282 for (LinkTuple linkIn : linkIdToLinkIn) { 283 long inLinkId = linkIn.link.getLinkId(); 284 long outLinkId = -inLinkId; 285 LinkTuple linkOut = linkIdToLinkOut.get(outLinkId); 286 if (linkOut == null) { 287 // TODO: log? 288 continue; 289 } 290 if (linkOut.markListId == linkIn.markListId) { 291 // continue; 292 } 293 // The name must be the same to match links together. 294 String name = 295 "link(" 296 + taskName(linkOut.lastTaskStart) 297 + " -> " 298 + taskName(linkIn.lastTaskStart) 299 + ")"; 300 long localUniqueLinkPairId = uniqueLinkPairId++; 301 traceEvents.add( 302 TraceEvent.EVENT 303 .name(name) 304 .tid(linkOut.threadId) 305 .pid(pid) 306 .phase("s") 307 .id(localUniqueLinkPairId) 308 .args(TraceEvent.TagMap.EMPTY.withKeyed("linkid", linkOut.link.getLinkId())) 309 .traceClockNanos(linkOut.lastTaskStart.getNanoTime() - initNanoTime)); 310 311 traceEvents.add( 312 TraceEvent.EVENT 313 .name(name) 314 .tid(linkIn.threadId) 315 .pid(pid) 316 .phase("t") 317 .id(localUniqueLinkPairId) 318 .args(TraceEvent.TagMap.EMPTY.withKeyed("linkid", linkOut.link.getLinkId())) 319 .traceClockNanos(linkIn.lastTaskStart.getNanoTime() - initNanoTime)); 320 } 321 super.exitGeneration(); 322 } 323 324 @Override enterMarkList(String threadName, long threadId, long markListId)325 protected void enterMarkList(String threadName, long threadId, long markListId) { 326 currentThreadId = threadId; 327 currentMarkListId = markListId; 328 traceEvents.add( 329 TraceEvent.EVENT 330 .name("thread_name") 331 .phase("M") 332 .pid(pid) 333 .args( 334 TraceEvent.TagMap.EMPTY 335 .withKeyed("name", threadName) 336 .withKeyed("markListId", markListId)) 337 .tid(currentThreadId)); 338 } 339 340 @Override onTaskStart(Mark mark, boolean unmatchedStart, boolean unmatchedEnd)341 protected void onTaskStart(Mark mark, boolean unmatchedStart, boolean unmatchedEnd) { 342 assert !(unmatchedStart && unmatchedEnd); 343 List<String> categories = Collections.emptyList(); 344 if (unmatchedStart) { 345 categories = Collections.singletonList("unknownStart"); 346 } else if (unmatchedEnd) { 347 categories = Collections.singletonList("unfinished"); 348 } 349 TraceEvent traceEvent = 350 TraceEvent.EVENT 351 .name(taskName(mark)) 352 .phase("B") 353 .pid(pid) 354 .categories(categories) 355 .tid(currentThreadId) 356 .traceClockNanos(mark.getNanoTime() - initNanoTime); 357 traceEvents.add(traceEvent); 358 taskStack.add(new TaskStart(mark, traceEvents.size() - 1)); 359 } 360 361 @Override 362 @SuppressWarnings("ReferenceEquality") // For checking if it's an empty end mark onTaskEnd(Mark mark, boolean unmatchedStart, boolean unmatchedEnd)363 protected void onTaskEnd(Mark mark, boolean unmatchedStart, boolean unmatchedEnd) { 364 assert !(unmatchedStart && unmatchedEnd); 365 List<String> categories = Collections.emptyList(); 366 if (unmatchedStart) { 367 categories = Collections.singletonList("unknownStart"); 368 } else if (unmatchedEnd) { 369 categories = Collections.singletonList("unfinished"); 370 } 371 // TODO: maybe copy the args from the start task 372 taskStack.pollLast(); 373 TraceEvent traceEvent = 374 TraceEvent.EVENT 375 .phase("E") 376 .pid(pid) 377 .categories(categories) 378 .tid(currentThreadId) 379 .traceClockNanos(mark.getNanoTime() - initNanoTime); 380 String name = taskName(mark); 381 if (name != MarkListWalker.UNKNOWN_TASK_NAME) { 382 traceEvent = traceEvent.name(name); 383 } 384 traceEvents.add(traceEvent); 385 } 386 387 @Override onAttachTag(Mark mark)388 protected void onAttachTag(Mark mark) { 389 if (taskStack.isEmpty()) { 390 // In a mark list of only links (i.e. no starts or ends) it's possible there are no tasks 391 // to bind to. This is probably due to not calling link() correctly. 392 logger.fine("Tag not associated with any task"); 393 return; 394 } 395 TaskStart taskStart = taskStack.peekLast(); 396 TraceEvent taskEvent = traceEvents.get(taskStart.traceEventIdx); 397 TraceEvent.TagMap args = taskEvent.args(); 398 out: 399 { 400 switch (mark.getOperation()) { 401 case TAG_N0S1: 402 args = args.withUnkeyed(mark.getTagStringValue(), Mark.NO_TAG_ID); 403 break out; 404 case TAG_N1S0: 405 args = args.withUnkeyed(Mark.NO_TAG_NAME, mark.getTagFirstNumeric()); 406 break out; 407 case TAG_N1S1: 408 args = args.withUnkeyed(mark.getTagStringValue(), mark.getTagFirstNumeric()); 409 break out; 410 case TAG_KEYED_N0S2: 411 args = args.withKeyed(mark.getTagKey(), mark.getTagStringValue()); 412 break out; 413 case TAG_KEYED_N1S1: 414 args = args.withKeyed(mark.getTagKey(), mark.getTagFirstNumeric()); 415 break out; 416 case TAG_KEYED_N2S1: 417 args = 418 args.withKeyed( 419 mark.getTagKey(), mark.getTagFirstNumeric(), mark.getTagSecondNumeric()); 420 break out; 421 case NONE: 422 case TASK_START_N1S1: 423 case TASK_START_N1S2: 424 case TASK_END_N1S0: 425 case TASK_END_N1S1: 426 case TASK_END_N1S2: 427 case EVENT_N1S1: 428 case EVENT_N1S2: 429 case EVENT_N2S2: 430 case EVENT_N2S3: 431 case LINK: 432 break; 433 } 434 throw new AssertionError(mark.getOperation()); 435 } 436 traceEvents.set(taskStart.traceEventIdx, taskEvent.args(args)); 437 } 438 439 @Override onEvent(Mark mark)440 protected void onEvent(Mark mark) { 441 TraceEvent.TagMap tagMap = TraceEvent.TagMap.EMPTY; 442 out: 443 { 444 switch (mark.getOperation()) { 445 case EVENT_N1S1: 446 case EVENT_N1S2: 447 break out; 448 case EVENT_N2S2: 449 case EVENT_N2S3: 450 tagMap = tagMap.withUnkeyed(mark.getTagStringValue(), mark.getTagFirstNumeric()); 451 break out; 452 case NONE: 453 case TASK_START_N1S1: 454 case TASK_START_N1S2: 455 case TASK_END_N1S0: 456 case TASK_END_N1S1: 457 case TASK_END_N1S2: 458 case LINK: 459 case TAG_N0S1: 460 case TAG_N1S0: 461 case TAG_N1S1: 462 case TAG_KEYED_N0S2: 463 case TAG_KEYED_N1S1: 464 case TAG_KEYED_N2S1: 465 break; 466 } 467 throw new AssertionError(mark.getOperation()); 468 } 469 TraceEvent traceEvent = 470 TraceEvent.EVENT 471 .name(taskName(mark)) 472 .phase("i") 473 .pid(pid) 474 .args(tagMap) 475 .tid(currentThreadId) 476 .traceClockNanos(mark.getNanoTime() - initNanoTime); 477 traceEvents.add(traceEvent); 478 } 479 480 static final class LinkTuple { 481 final Mark lastTaskStart; 482 final Mark link; 483 final long threadId; 484 final long markListId; 485 LinkTuple(Mark lastTaskStart, Mark link, long threadId, long markListId)486 LinkTuple(Mark lastTaskStart, Mark link, long threadId, long markListId) { 487 this.lastTaskStart = lastTaskStart; 488 this.link = link; 489 this.threadId = threadId; 490 this.markListId = markListId; 491 } 492 } 493 494 @Override onLink(Mark mark)495 protected void onLink(Mark mark) { 496 if (taskStack.isEmpty()) { 497 // In a mark list of only links (i.e. no starts or ends) it's possible there are no tasks 498 // to bind to. This is probably due to not calling link() correctly. 499 logger.fine("Link not associated with any task"); 500 return; 501 } 502 LinkTuple linkTuple = 503 new LinkTuple(taskStack.peekLast().mark, mark, currentThreadId, currentMarkListId); 504 if (mark.getLinkId() > 0) { 505 LinkTuple old = linkIdToLinkOut.put(mark.getLinkId(), linkTuple); 506 assert old == null; 507 } else if (mark.getLinkId() < 0) { 508 linkIdToLinkIn.add(linkTuple); 509 } 510 } 511 } 512 taskName(Mark mark)513 private static String taskName(Mark mark) { 514 switch (mark.getOperation()) { 515 case TASK_END_N1S0: 516 return MarkListWalker.UNKNOWN_TASK_NAME; 517 case TASK_START_N1S1: 518 case TASK_END_N1S1: 519 case EVENT_N1S1: 520 case EVENT_N2S2: 521 return mark.getTaskName(); 522 case TASK_START_N1S2: 523 case TASK_END_N1S2: 524 case EVENT_N1S2: 525 case EVENT_N2S3: 526 return mark.getTaskName() + '.' + mark.getSubTaskName(); 527 case LINK: 528 case TAG_N0S1: 529 case TAG_KEYED_N0S2: 530 case TAG_KEYED_N1S1: 531 case TAG_KEYED_N2S1: 532 case TAG_N1S0: 533 case TAG_N1S1: 534 case NONE: 535 throw new UnsupportedOperationException(mark.toString()); 536 } 537 throw new AssertionError(mark.getOperation()); 538 } 539 } 540