XCTraceNormProfilerTest.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.it.profilers;

import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;
import org.openjdk.jmh.it.Fixtures;
import org.openjdk.jmh.profile.ProfilerException;
import org.openjdk.jmh.profile.XCTraceNormProfiler;
import org.openjdk.jmh.results.Result;
import org.openjdk.jmh.results.RunResult;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.util.FileUtils;
import org.openjdk.jmh.util.Utils;

import java.io.File;
import java.util.*;

public class XCTraceNormProfilerTest extends AbstractAsmProfilerTest {
    private static boolean xctraceExists() {
        Collection<String> out = Utils.tryWith("xcode-select", "-p");
        if (!out.isEmpty()) {
            return false;
        }
        Optional<String> path = Utils.runWith("xcode-select", "-p").stream()
                .flatMap(line -> Arrays.stream(line.split("\n")))
                .findFirst();
        if (!path.isPresent()) {
            return false;
        }
        File xctraceExe = new File(path.get(), "usr/bin/xctrace");
        if (!xctraceExe.exists()) {
            return false;
        }

        Collection<String> versionOut = Utils.runWith(xctraceExe.getAbsolutePath(), "version");
        Optional<String> versionStr = versionOut.stream().flatMap(l -> Arrays.stream(l.split("\n")))
                .filter(l -> l.contains("xctrace version "))
                .findAny();
        if (!versionStr.isPresent()) {
            return false;
        }
        try {
            int version = Integer.parseInt(versionStr.get()
                    .split("version ")[1]
                    .split(" ")[0]
                    .split("\\.")[0]);
            return version >= 13;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    private static boolean isInsideVM() {
        // Alternatively, we can check if CPC subsystem is up and running (kern.cpc.secure)
        String vmmPresent = Utils.runWith("sysctl", "-n", "kern.hv_vmm_present")
                .iterator()
                .next()
                .split("\n")[0];
        // It's either 0 or 1 when sysctl property exists, some error string otherwise
        return vmmPresent.equals("1");
    }

    private static void skipIfProfilerNotSupport() {
        Assume.assumeTrue(xctraceExists());
    }

    private static void skipIfRunningInsideVirtualMachine() {
        Assume.assumeFalse(isInsideVM());
    }

    private void checkProfiling(int forks) throws RunnerException {
        Options opts = new OptionsBuilder()
                .include(Fixtures.getTestMask(this.getClass()))
                .addProfiler(XCTraceNormProfiler.class)
                .forks(forks)
                .build();

        RunResult rr = new Runner(opts).runSingle();

        Map<String, Result> sr = rr.getSecondaryResults();
        double instructions = checkedGetAny(sr, "Instructions",
                "FIXED_INSTRUCTIONS", "INST_ALL", "INST_RETIRED.ANY", "INST_RETIRED.ANY_P");
        double cycles = checkedGetAny(sr, "Cycles", "FIXED_CYCLES",
                "CORE_ACTIVE_CYCLE", "CPU_CLK_UNHALTED.THREAD", "CPU_CLK_UNHALTED.THREAD_P");
        double branches = checkedGetAny(sr, "INST_BRANCH", "BR_INST_RETIRED.ALL_BRANCHES",
                "BR_INST_RETIRED.ALL_BRANCHES_PEBS");
        double missedBranches = checkedGetAny(sr, "BRANCH_MISPRED_NONSPEC", "BR_MISP_RETIRED.ALL_BRANCHES",
                "BR_MISP_RETIRED.ALL_BRANCHES_PS");

        Assert.assertNotEquals(0D, instructions, 0D);
        Assert.assertNotEquals(0D, cycles, 0D);
        Assert.assertNotEquals(0D, branches, 0D);
        Assert.assertNotEquals(0D, missedBranches, 0D);

        double cpi = ProfilerTestUtils.checkedGet(sr, "CPI").getScore();
        double ipc = ProfilerTestUtils.checkedGet(sr, "IPC").getScore();
        double branchMissRatio = ProfilerTestUtils.checkedGet(sr, "Branch miss ratio").getScore();

        Assert.assertNotEquals(0D, ipc, 0D);
        Assert.assertNotEquals(0D, cpi, 0D);
        Assert.assertNotEquals(0D, branchMissRatio, 0D);
    }

    @Test
    public void testDefaultArguments() throws Exception {
        skipIfProfilerNotSupport();
        skipIfRunningInsideVirtualMachine();

        checkProfiling(1);
    }

    @Test
    public void testFailWithNonExistentTemplate() {
        skipIfProfilerNotSupport();

        Options opts = new OptionsBuilder()
                .include(Fixtures.getTestMask(this.getClass()))
                .addProfiler(XCTraceNormProfiler.class, "template=NON_EXISTENT_TEMPLATE")
                .forks(1)
                .build();
        Assert.assertThrows("No results returned", RunnerException.class, () -> new Runner(opts).runSingle());
    }

    @Test
    public void testUnsupportedTemplate() {
        skipIfProfilerNotSupport();
        skipIfRunningInsideVirtualMachine();

        Options opts = new OptionsBuilder()
                .include(Fixtures.getTestMask(this.getClass()))
                .addProfiler(XCTraceNormProfiler.class, "template=CPU Profiler")
                .forks(1)
                .build();
        Assert.assertThrows("Table \"counters-profile\" was not found in the trace results.",
                IllegalStateException.class, () -> new Runner(opts).runSingle());
    }

    @Test
    public void testUseCustomTemplate() throws Exception {
        skipIfProfilerNotSupport();
        skipIfRunningInsideVirtualMachine();

        RunResult result;
        File templateFile = FileUtils.extractFromResource("/default.instruments.template.xml");
        Options opts = new OptionsBuilder()
                .include(Fixtures.getTestMask(this.getClass()))
                .addProfiler(XCTraceNormProfiler.class, "template=" + templateFile.getAbsolutePath())
                .forks(1)
                .build();
        try {
            result = new Runner(opts).runSingle();
        } finally {
            templateFile.delete();
        }

        Map<String, Result> sr = result.getSecondaryResults();
        double instructions = checkedGetAny(sr, "Instructions",
                "FIXED_INSTRUCTIONS", "INST_ALL", "INST_RETIRED.ANY", "INST_RETIRED.ANY_P");
        Assert.assertNotEquals(0D, instructions, 0D);
    }

    @Test
    public void testMultipleForks() throws Exception {
        skipIfProfilerNotSupport();
        skipIfRunningInsideVirtualMachine();

        checkProfiling(2);
    }

    @Test
    public void testConstructorThrowsWhenXCTraceDoesNotExist() {
        Assume.assumeFalse(xctraceExists());
        Assert.assertThrows(ProfilerException.class, () -> new XCTraceNormProfiler(""));
    }

    private static double checkedGetAny(Map<String, Result> results, String... keys) {
        for (String key : keys) {
            Result value = results.get(key);
            if (value != null) return value.getScore();
        }
        throw new IllegalStateException(
                "Results does not include any of these keys: " + String.join(", ", keys));
    }
}