XCTraceSupport.java

/*
 * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package org.openjdk.jmh.profile;

import org.openjdk.jmh.util.Utils;

import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

final class XCTraceSupport {
    private static final int ANY_VERSION = 0;

    private XCTraceSupport() {
    }

    static void exportTable(String xctracePath, String runFile, String outputFile,
                            XCTraceTableHandler.ProfilingTableType table) {
        Collection<String> out = Utils.tryWith(
                xctracePath, "export",
                "--input", runFile,
                "--output", outputFile,
                "--xpath",
                "/trace-toc/run/data/table[@schema=\"" + table.tableName + "\"]"
        );
        if (!out.isEmpty()) {
            throw new IllegalStateException(out.toString());
        }
    }

    static void exportTableOfContents(String xctracePath, String runFile, String outputFile) {
        Collection<String> out = Utils.tryWith(
                xctracePath, "export",
                "--input", runFile,
                "--output", outputFile,
                "--toc"
        );
        if (!out.isEmpty()) {
            throw new IllegalStateException(out.toString());
        }
    }

    static Collection<String> recordCommandPrefix(String xctracePath, String runFile, String template) {
        return Arrays.asList(
                xctracePath, "record",
                "--template", template,
                "--output", runFile,
                "--target-stdout", "-",
                "--launch", "--"
        );
    }

    /**
     * Returns absolute path to xctrace executable or throws ProfilerException if it does not exist.
     * <p>
     * xctrace is expected to be at $(xcode-select -p)/usr/bin/xctrace
     */
    static String getXCTracePath() throws ProfilerException {
        return getXCTracePath(ANY_VERSION);
    }

    static Path getXCodeDevToolsPath() throws ProfilerException {
        Collection<String> out = Utils.tryWith("xcode-select", "-p");
        if (!out.isEmpty()) {
            throw new ProfilerException("\"xcode-select -p\" failed: " + out);
        }
        out = Utils.runWith("xcode-select", "-p");
        String devPath = out.stream().flatMap(l -> Arrays.stream(l.split("\n"))).findFirst().orElseThrow(
                () -> new ProfilerException("\"xcode-select -p\" output is empty"));
        return Paths.get(devPath);
    }

    /**
     * Returns absolute path to xctrace executable or throws ProfilerException if it does not exist
     * or its version is below {@code minVersion}.
     * <p>
     * xctrace is expected to be at {@code $(xcode-select -p)/usr/bin/xctrace}
     *
     * @param minVersion a minimum required major xctrace version, like {@code 13}. Use {@code 0} to allow any version.
     */
    static String getXCTracePath(int minVersion) throws ProfilerException {
        File xctrace = getXCodeDevToolsPath().resolve(Paths.get("usr", "bin", "xctrace")).toFile();
        String xctracePath = xctrace.getAbsolutePath();
        if (!xctrace.exists()) {
            throw new ProfilerException("xctrace was not found at " + xctracePath);
        }
        Collection<String> versionOut = Utils.runWith(xctracePath, "version");
        String versionString = versionOut.stream().flatMap(l -> Arrays.stream(l.split("\n")))
                .filter(l -> l.contains("xctrace version"))
                .findFirst()
                .orElseThrow(() -> new ProfilerException("\"xctrace version\" failed: " + versionOut));

        checkVersion(versionString, minVersion);

        return xctrace.getAbsolutePath();
    }

    private static void checkVersion(String versionString, int minVersion) throws ProfilerException {
        String extractedVersion = versionString.split("xctrace version ")[1].split(" ")[0];
        int majorVersion = Integer.parseInt(extractedVersion.split("\\.")[0]);

        if (majorVersion < minVersion) {
            throw new ProfilerException(
                    "xctrace version (" + versionString + ") is too low (required at least " + minVersion + ").");
        }
    }

    static Path findTraceFile(Path parent) {
        try (Stream<Path> files = Files.list(parent)) {
            List<Path> launchFiles = files
                    .filter(path -> path.getFileName().toString().startsWith("Launch"))
                    .collect(Collectors.toList());
            if (launchFiles.size() != 1) {
                throw new IllegalStateException("Expected only one launch file, found " +
                        launchFiles.size() + ": " + launchFiles);
            }
            return launchFiles.get(0);
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
    }

    static void removeDirectory(Path path) {
        if (!path.toFile().exists()) {
            return;
        }
        try {
            Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    Files.delete(file);
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                    Files.delete(dir);
                    return FileVisitResult.CONTINUE;
                }
            });
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
    }

    static Path createTemporaryDirectoryName() {
        // In general, it's unsafe to create a random file name and then create a file/dir itself.
        // But it should be fine for profiling purposes.
        String tempDir = System.getProperty("java.io.tmpdir");
        if (tempDir == null) {
            throw new IllegalStateException("System temporary folder is unknown.");
        }
        for (int i = 0; i < 5; i++) {
            String dirname = "jmh-xctrace-results-" + System.nanoTime();
            Path path = Paths.get(tempDir, dirname);
            if (!path.toFile().exists()) {
                return path;
            }
        }
        throw new IllegalStateException("Can't create a temporary folder for a run.");
    }

    static void copyDirectory(Path source, Path destination) throws IOException {
        Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                Path destPath = destination.resolve(source.relativize(dir));
                Files.copy(dir, destPath);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Path destFilePath = destination.resolve(source.relativize(file));
                Files.copy(file, destFilePath, StandardCopyOption.COPY_ATTRIBUTES);
                return FileVisitResult.CONTINUE;
            }
        });
    }
}