lttng.ust: Do not skip unknown lines in addr2line output
[deliverable/tracecompass.git] / lttng / org.eclipse.tracecompass.lttng2.ust.core / src / org / eclipse / tracecompass / internal / lttng2 / ust / core / analysis / debuginfo / FileOffsetMapper.java
index 017888a29af7333dfc7d5a6459b5f4e03dcd5687..d6700046d216c57145fa101fa2138b8ee10141d5 100644 (file)
@@ -17,13 +17,23 @@ import java.io.IOException;
 import java.io.InputStreamReader;
 import java.nio.file.Files;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.logging.Logger;
 import java.util.stream.Collectors;
 
+import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.common.core.log.TraceCompassLog;
 import org.eclipse.tracecompass.tmf.core.event.lookup.TmfCallsite;
 
+import com.google.common.base.Objects;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Iterables;
+
 /**
  * Utility class to get file name, function/symbol name and line number from a
  * given offset. In TMF this is represented as a {@link TmfCallsite}.
@@ -32,56 +42,243 @@ import org.eclipse.tracecompass.tmf.core.event.lookup.TmfCallsite;
  */
 public final class FileOffsetMapper {
 
-    private static final String DISCRIMINATOR = "\\(discriminator.*\\)";
+    private static final Logger LOGGER = TraceCompassLog.getLogger(FileOffsetMapper.class);
+
+    private static final String DISCRIMINATOR = "\\(discriminator.*\\)"; //$NON-NLS-1$
     private static final String ADDR2LINE_EXECUTABLE = "addr2line"; //$NON-NLS-1$
 
+    private static final long CACHE_SIZE = 1000;
+
     private FileOffsetMapper() {}
 
     /**
-     * Generate the callsites from a given binary file and address offset.
+     * Class representing an offset in a specific file
+     */
+    private static class FileOffset {
+
+        private final String fFilePath;
+        private final @Nullable String fBuildId;
+        private final long fOffset;
+
+        public FileOffset(String filePath, @Nullable String buildId, long offset) {
+            fFilePath = filePath;
+            fBuildId = buildId;
+            fOffset = offset;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(fFilePath, fBuildId, fOffset);
+        }
+
+        @Override
+        public boolean equals(@Nullable Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            FileOffset other = (FileOffset) obj;
+            return Objects.equal(fFilePath, other.fFilePath) &&
+                    Objects.equal(fBuildId, other.fBuildId) &&
+                    Objects.equal(fOffset, other.fOffset);
+        }
+
+        @Override
+        public String toString() {
+            return Objects.toStringHelper(this)
+                    .add("fFilePath", fFilePath) //$NON-NLS-1$
+                    .add("fBuildId", fBuildId) //$NON-NLS-1$
+                    .add("fOffset", String.format("0x%h", fOffset)) //$NON-NLS-1$ //$NON-NLS-2$
+                    .toString();
+        }
+    }
+
+
+    /**
+     * Generate the callsite from a given binary file and address offset.
      *
      * Due to function inlining, it is possible for one offset to actually have
-     * multiple call sites. This is why we can return more than one callsite per
-     * call.
+     * multiple call sites. We will return the most precise one (inner-most) we
+     * have available.
+     *
+     * @param file
+     *            The binary file to look at
+     * @param buildId
+     *            The expected buildId of the binary file (is not verified at
+     *            the moment)
+     * @param offset
+     *            The memory offset in the file
+     * @return The corresponding call site
+     */
+    public static @Nullable TmfCallsite getCallsiteFromOffset(File file, @Nullable String buildId, long offset) {
+       Iterable<Addr2lineInfo> output = getAddr2lineInfo(file, buildId, offset);
+       if (output == null || Iterables.isEmpty(output)) {
+           return null;
+       }
+       Addr2lineInfo info = Iterables.getLast(output);
+       String sourceFile = info.fSourceFileName;
+       Long sourceLine = info.fSourceLineNumber;
+
+       if (sourceFile == null) {
+           /* Not enough information to provide a callsite */
+           return null;
+       }
+       return new TmfCallsite(sourceFile, sourceLine);
+    }
+
+    /**
+     * Get the function/symbol name corresponding to binary file and offset.
      *
      * @param file
      *            The binary file to look at
+     * @param buildId
+     *            The expected buildId of the binary file (is not verified at
+     *            the moment)
      * @param offset
      *            The memory offset in the file
-     * @return The list of callsites corresponding to the offset, reported from
-     *         the "highest" inlining location, down to the initial definition.
+     * @return The corresponding function/symbol name
      */
-    public static @Nullable Iterable<TmfCallsite> getCallsiteFromOffset(File file, long offset) {
+    public static @Nullable String getFunctionNameFromOffset(File file, @Nullable String buildId, long offset) {
+        /*
+         * TODO We are currently also using 'addr2line' to resolve function
+         * names, which requires the binary's DWARF information to be available.
+         * A better approach would be to use the binary's symbol table (if it is
+         * not stripped), since this is usually more readily available than
+         * DWARF.
+         */
+        Iterable<Addr2lineInfo> output = getAddr2lineInfo(file, buildId, offset);
+        if (output == null || Iterables.isEmpty(output)) {
+            return null;
+        }
+        Addr2lineInfo info = Iterables.getLast(output);
+        return info.fFunctionName;
+    }
+
+    // ------------------------------------------------------------------------
+    // Utility methods making use of 'addr2line'
+    // ------------------------------------------------------------------------
+
+    /**
+     * Value used in addr2line output to represent unknown function names or
+     * source files.
+     */
+    private static final String UNKNOWN_VALUE = "??"; //$NON-NLS-1$
+
+    /**
+     * Cache of all calls to 'addr2line', so that we can avoid recalling the
+     * external process repeatedly.
+     *
+     * It is static, meaning one cache for the whole application, since the
+     * symbols in a file on disk are independent from the trace referring to it.
+     */
+    private static final LoadingCache<FileOffset, @NonNull Iterable<Addr2lineInfo>> ADDR2LINE_INFO_CACHE;
+    static {
+        ADDR2LINE_INFO_CACHE = checkNotNull(CacheBuilder.newBuilder()
+            .maximumSize(CACHE_SIZE)
+            .build(new CacheLoader<FileOffset, @NonNull Iterable<Addr2lineInfo>>() {
+                @Override
+                public @NonNull Iterable<Addr2lineInfo> load(FileOffset fo) {
+                    LOGGER.fine(() -> "[FileOffsetMapper:CacheMiss] file/offset=" + fo.toString()); //$NON-NLS-1$
+                    return callAddr2line(fo);
+                }
+            }));
+    }
+
+    private static class Addr2lineInfo {
+
+        private final @Nullable String fSourceFileName;
+        private final @Nullable Long fSourceLineNumber;
+        private final @Nullable String fFunctionName;
+
+        public Addr2lineInfo(@Nullable String sourceFileName,  @Nullable String functionName, @Nullable Long sourceLineNumber) {
+            fSourceFileName = sourceFileName;
+            fSourceLineNumber = sourceLineNumber;
+            fFunctionName = functionName;
+        }
+
+        @Override
+        public String toString() {
+            return Objects.toStringHelper(this)
+                    .add("fSourceFileName", fSourceFileName) //$NON-NLS-1$
+                    .add("fSourceLineNumber", fSourceLineNumber) //$NON-NLS-1$
+                    .add("fFunctionName", fFunctionName) //$NON-NLS-1$
+                    .toString();
+        }
+    }
+
+    private static @Nullable Iterable<Addr2lineInfo> getAddr2lineInfo(File file, @Nullable String buildId, long offset) {
+        LOGGER.finer(() -> String.format("[FileOffsetMapper:Addr2lineRequest] file=%s, buildId=%s, offset=0x%h", //$NON-NLS-1$
+                file.toString(), buildId, offset));
+
         if (!Files.exists((file.toPath()))) {
+            LOGGER.finer(() -> "[FileOffsetMapper:RequestFailed] File not found"); //$NON-NLS-1$
             return null;
         }
-        return getCallsiteFromOffsetWithAddr2line(file, offset);
+        // TODO We should also eventually verify that the passed buildId matches
+        // the file we are attempting to open.
+        FileOffset fo = new FileOffset(checkNotNull(file.toString()), buildId, offset);
+
+        @Nullable Iterable<Addr2lineInfo> callsites = ADDR2LINE_INFO_CACHE.getUnchecked(fo);
+        LOGGER.finer(() -> String.format("[FileOffsetMapper:RequestComplete] callsites=%s", callsites)); //$NON-NLS-1$
+        return callsites;
     }
 
-    private static @Nullable Iterable<TmfCallsite> getCallsiteFromOffsetWithAddr2line(File file, long offset) {
-        List<TmfCallsite> callsites = new LinkedList<>();
+    private static Iterable<Addr2lineInfo> callAddr2line(FileOffset fo) {
+        String filePath = fo.fFilePath;
+        long offset = fo.fOffset;
+
+        List<Addr2lineInfo> callsites = new LinkedList<>();
 
-        // FIXME Could eventually use CDT's Addr2line class once it imlements --inlines
-        List<String> output = getOutputFromCommand(checkNotNull(Arrays.asList(
-                ADDR2LINE_EXECUTABLE, "-i", "-e", file.toString(), "0x" + Long.toHexString(offset))));  //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
+        // FIXME Could eventually use CDT's Addr2line class once it implements --inlines
+        List<String> output = getOutputFromCommand(Arrays.asList(
+                ADDR2LINE_EXECUTABLE, "-i", "-f", "-C", "-e", filePath, "0x" + Long.toHexString(offset)));  //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
 
         if (output == null) {
             /* Command returned an error */
-            return null;
+            return Collections.EMPTY_SET;
         }
 
+        /*
+         * When passing the -f flag, the output alternates between function
+         * names and file/line location.
+         */
+        boolean oddLine = false; // We flip at the start, first loop will be odd
+        String currentFunctionName = null;
         for (String outputLine : output) {
+            /* Flip the boolean for the following line */
+            oddLine = !oddLine;
+
             // Remove discriminator part, for example: /build/buildd/glibc-2.21/elf/dl-object.c:78 (discriminator 8)
             outputLine = outputLine.replaceFirst(DISCRIMINATOR, "").trim(); //$NON-NLS-1$
 
-            String[] elems = outputLine.split(":"); //$NON-NLS-1$
-            String fileName = elems[0];
-            if (fileName.equals("??")) { //$NON-NLS-1$
-                continue;
+            if (oddLine) {
+                /* This is a line indicating the function name */
+                if (outputLine.equals(UNKNOWN_VALUE)) {
+                    currentFunctionName = null;
+                } else {
+                    currentFunctionName = outputLine;
+                }
+            } else {
+                /* This is a line indicating a call site */
+                String[] elems = outputLine.split(":"); //$NON-NLS-1$
+                String fileName = elems[0];
+                if (fileName.equals(UNKNOWN_VALUE)) {
+                    fileName = null;
+                }
+                Long lineNumber;
+                try {
+                    lineNumber = Long.valueOf(elems[1]);
+                } catch (NumberFormatException e) {
+                    /* Probably a '?' output, meaning unknown line number. */
+                    lineNumber = null;
+                }
+                callsites.add(new Addr2lineInfo(fileName, currentFunctionName, lineNumber));
             }
-            long lineNumber = Long.parseLong(elems[1]);
-
-            callsites.add(new TmfCallsite(fileName, null, lineNumber));
         }
 
         return callsites;
@@ -93,12 +290,12 @@ public final class FileOffsetMapper {
             builder.redirectErrorStream(true);
 
             Process p = builder.start();
-            BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
-            int ret = p.waitFor();
-            List<String> lines = br.lines().collect(Collectors.toList());
-
-            return (ret == 0 ? lines : null);
+            try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));) {
+                int ret = p.waitFor();
+                List<String> lines = br.lines().collect(Collectors.toList());
 
+                return (ret == 0 ? lines : null);
+            }
         } catch (IOException | InterruptedException e) {
             return null;
         }
This page took 0.042362 seconds and 5 git commands to generate.