• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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