diff --git a/.gitignore b/.gitignore index 2f7896d..a43dfcf 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ target/ +jit-*.dump +jitted-*.so +perf.*data* diff --git a/README.md b/README.md index 4455790..93dfad9 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,46 @@ perf map file would be written at [shutdown hook](https://docs.oracle.com/en/jav You need to enable perf map dumper via `CodeSegment::enablePerfMapDumper`. Call `CodeSegment::disablePerfMapDumper` if you want to cancel the dumper. +## Generate jitdump + +perf tool on Linux supports JIT-generated code. ffmasm can dump generated code as a jitdump. See an [example](examples/perf) for details. + +### Record assembled code as a JIT'ed code + +Pass `JitDump` insntace to `build` method. + +```java +jitdump = JitDump.getInstance(Path.of(".")); + + : + +.build("GeneratedFunc", jitdump); +``` + +Then you can run `perf record`. Note that you have to set monotonic clock with `-k` option. + +``` +perf record -k 1 $JAVA_HOME/bin/java ... +``` + +As a result, you would get `jit-.dump` which includes JIT information. You should keep until run `perf inject`. + +### Inject JIT'ed code into recording file + +`perf.data` generated by `perf record` would not include JIT'ed code, so you need to inject them via `perf inject` as following. + +``` +perf inject --jit -i perf.data -o perf.jit.data +``` + +You will get some `.so` file and `perf.jit.data` as an injected file as a result. + +### Check with `perf report` + +``` +perf report -i perf.jit.data +``` + # License The GNU Lesser General Public License, version 3.0 diff --git a/examples/perf/README.md b/examples/perf/README.md new file mode 100644 index 0000000..562f127 --- /dev/null +++ b/examples/perf/README.md @@ -0,0 +1,31 @@ +Example of profiling with perf tool +=================== + +This example shows how to use `JitDump` to profile your assembly code generated by ffmasm. [PerfJit.java](src/main/java/com/yasuenag/ffmasm/examples/perf/PerfJit.java) issues `RDRAND` instruction in 10,000,000 times. + +# How to build + +``` +cd /path/to/ffasm +mvn install +cd examples/perf +mvn package +``` + +# Run test + +[perf.sh](perf.sh) runs the example with `perf` and injects jitted code (it means assembly code generated by ffmasm) with `perf inject`. + +``` +./perf.sh +``` + +You can see `jit-.dump`, `jitted-*.so`, and `perf.*data*` on working directory of `perf.sh`. Please keep them until you run `perf report`. + +# Profiling with `perf report` + +You need to specify `perf.jit.data` with `-i` option to load injected data. + +``` +perf report -i perf.jit.data +``` diff --git a/examples/perf/perf.sh b/examples/perf/perf.sh new file mode 100755 index 0000000..367ee53 --- /dev/null +++ b/examples/perf/perf.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +BASEDIR=$(dirname $0) + +perf record -k 1 $JAVA_HOME/bin/java -jar $BASEDIR/target/perf-example-0.1.0.jar && \ + perf inject --jit -i perf.data -o perf.jit.data && \ + echo 'Completed. run "perf report -i perf.jit.data"' diff --git a/examples/perf/pom.xml b/examples/perf/pom.xml new file mode 100644 index 0000000..67066cc --- /dev/null +++ b/examples/perf/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + com.yasuenag + perf-example + jar + 0.1.0 + + + UTF-8 + 22 + 22 + + + + + com.yasuenag + ffmasm + 0.4.0-SNAPSHOT + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + -Xlint:all + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + + ALL-UNNAMED + + + + + + maven-shade-plugin + 3.6.0 + + false + + + + package + + shade + + + + + com.yasuenag.ffmasm.examples.perf.PerfJit + + + + + + + + + + diff --git a/examples/perf/src/main/java/com/yasuenag/ffmasm/examples/perf/PerfJit.java b/examples/perf/src/main/java/com/yasuenag/ffmasm/examples/perf/PerfJit.java new file mode 100644 index 0000000..d73cd55 --- /dev/null +++ b/examples/perf/src/main/java/com/yasuenag/ffmasm/examples/perf/PerfJit.java @@ -0,0 +1,46 @@ +package com.yasuenag.ffmasm.examples.perf; + +import java.lang.foreign.*; +import java.lang.invoke.*; +import java.nio.file.*; +import java.util.*; + +import com.yasuenag.ffmasm.*; +import com.yasuenag.ffmasm.amd64.*; + + +public class PerfJit{ + + private static final CodeSegment seg; + + private static final MethodHandle rdtsc; + + private static final JitDump jitdump; + + static{ + try{ + seg = new CodeSegment(); + jitdump = JitDump.getInstance(Path.of(".")); + var desc = FunctionDescriptor.of(ValueLayout.JAVA_LONG); + rdtsc = AMD64AsmBuilder.create(AMD64AsmBuilder.class, seg, desc) + /* .align 16 */ .alignTo16BytesWithNOP() + /* retry: */ .label("retry") + /* rdrand %rax */ .rdrand(Register.RAX) + /* jae retry */ .jae("retry") + /* ret */ .ret() + .build("ffm_rdtsc", jitdump, Linker.Option.critical(false)); + } + catch(Throwable t){ + throw new RuntimeException(t); + } + } + + public static void main(String[] args) throws Throwable{ + try(jitdump; seg){ + for(int i = 0; i < 10_000_000; i++){ + long _ = (long)rdtsc.invokeExact(); + } + } + } + +} diff --git a/src/main/java/com/yasuenag/ffmasm/CodeSegment.java b/src/main/java/com/yasuenag/ffmasm/CodeSegment.java index b5d7a23..04a16eb 100644 --- a/src/main/java/com/yasuenag/ffmasm/CodeSegment.java +++ b/src/main/java/com/yasuenag/ffmasm/CodeSegment.java @@ -188,18 +188,21 @@ public MemorySegment getAddr(){ } /** - * Add method info to the list which should be dumped as perf map. - * - * @param mh MethodHandle should be recorded - * @param name method name - * @param address top address of the method - * @param size method size + * Add method info. It will be dumped to perf map as related method of this CodeSegment. + * @param mh MethodHandle of the method + * @param name Method name + * @param address Address of the method + * @param size Size of the method (machine code) + * @return MethodInfo of the method info. + * @throws IllegalArgumentException if the address is out of range from this CodeSegment. */ - public void addMethodInfo(MethodHandle mh, String name, long address, int size){ + public MethodInfo addMethodInfo(MethodHandle mh, String name, long address, int size){ if((address < addr.address()) || ((addr.address() + this.size) < (address + size))){ throw new IllegalArgumentException("Address is out of range from CodeSegment."); } - methods.add(new MethodInfo(mh, name, address, size)); + var methodInfo = new MethodInfo(mh, name, address, size); + methods.add(methodInfo); + return methodInfo; } private void dumpPerfMap(Path path){ diff --git a/src/main/java/com/yasuenag/ffmasm/JitDump.java b/src/main/java/com/yasuenag/ffmasm/JitDump.java new file mode 100644 index 0000000..5489562 --- /dev/null +++ b/src/main/java/com/yasuenag/ffmasm/JitDump.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024, Yasumasa Suenaga + * + * This file is part of ffmasm. + * + * ffmasm is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ffmasm 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ffmasm. If not, see . + */ +package com.yasuenag.ffmasm; + +import java.io.IOException; +import java.nio.file.Path; + +import com.yasuenag.ffmasm.internal.linux.PerfJitDump; + + +/** + * Interface of jitdump for perf command on Linux. + * + * @author Yasumasa Suenaga + */ +public interface JitDump extends AutoCloseable{ + + /** + * Get instance of JitDump. + * + * @param dir Base directory which jitdump is generated. + * @throws UnsupportedPlatformException if the call happens on unsupported platform. + */ + public static JitDump getInstance(Path dir) throws UnsupportedPlatformException, PlatformException, IOException{ + var osName = System.getProperty("os.name"); + if(osName.equals("Linux")){ + return new PerfJitDump(dir); + } + + throw new UnsupportedPlatformException("This platform is not supported in JitDump"); + } + + /** + * Write method info to jitdump. + * + * @param method MethodInfo should be written. + */ + public void writeFunction(CodeSegment.MethodInfo method); + +} diff --git a/src/main/java/com/yasuenag/ffmasm/amd64/AMD64AsmBuilder.java b/src/main/java/com/yasuenag/ffmasm/amd64/AMD64AsmBuilder.java index 695b5b9..371d803 100644 --- a/src/main/java/com/yasuenag/ffmasm/amd64/AMD64AsmBuilder.java +++ b/src/main/java/com/yasuenag/ffmasm/amd64/AMD64AsmBuilder.java @@ -33,6 +33,7 @@ import java.util.Set; import com.yasuenag.ffmasm.CodeSegment; +import com.yasuenag.ffmasm.JitDump; import com.yasuenag.ffmasm.UnsupportedPlatformException; @@ -976,17 +977,34 @@ public MethodHandle build(Linker.Option... options){ /** * Build as a MethodHandle * + * @param name Method name * @param options Linker options to pass to downcallHandle(). * @return MethodHandle for this assembly * @throws IllegalStateException when label(s) are not defined even if they are used */ public MethodHandle build(String name, Linker.Option... options){ + return build(name, null, options); + } + + /** + * Build as a MethodHandle + * + * @param name Method name + * @param jitdump JitDump instance which should be written. + * @param options Linker options to pass to downcallHandle(). + * @return MethodHandle for this assembly + * @throws IllegalStateException when label(s) are not defined even if they are used + */ + public MethodHandle build(String name, JitDump jitdump, Linker.Option... options){ updateTail(); var top = mem.address(); var size = byteBuf.position(); var mh = Linker.nativeLinker().downcallHandle(mem, desc, options); - seg.addMethodInfo(mh, name, top, size); + var methodInfo = seg.addMethodInfo(mh, name, top, size); + if(jitdump != null){ + jitdump.writeFunction(methodInfo); + } return mh; } diff --git a/src/main/java/com/yasuenag/ffmasm/internal/linux/LinuxExecMemory.java b/src/main/java/com/yasuenag/ffmasm/internal/linux/LinuxExecMemory.java index 24e1239..0d5776b 100644 --- a/src/main/java/com/yasuenag/ffmasm/internal/linux/LinuxExecMemory.java +++ b/src/main/java/com/yasuenag/ffmasm/internal/linux/LinuxExecMemory.java @@ -51,11 +51,11 @@ public class LinuxExecMemory implements ExecMemory{ private static final MemorySegment errnoSeg; - private MethodHandle hndMmap = null; + private static MethodHandle hndMmap = null; - private MethodHandle hndMunmap = null; + private static MethodHandle hndMunmap = null; - private VarHandle hndErrno = null; + private static VarHandle hndErrno = null; /** * page can be read @@ -90,7 +90,12 @@ public class LinuxExecMemory implements ExecMemory{ errnoSeg = Arena.global().allocate(Linker.Option.captureStateLayout()); } - private MemorySegment mmap(MemorySegment addr, long length, int prot, int flags, int fd, long offset) throws PlatformException{ + /** + * Call mmap(2) via FFM. See manpage of mmap(2) for details. + * + * @throws PlatformException if mmap(2) or FFM call failed. + */ + public static MemorySegment mmap(MemorySegment addr, long length, int prot, int flags, int fd, long offset) throws PlatformException{ if(hndMmap == null){ var func = sym.find("mmap").get(); var desc = FunctionDescriptor.of( @@ -120,7 +125,12 @@ private MemorySegment mmap(MemorySegment addr, long length, int prot, int flags, } } - private int munmap(MemorySegment addr, long length) throws PlatformException{ + /** + * Call munmap(2) via FFM. See manpage of munmap(2) for details. + * + * @throws PlatformException if munmap(2) or FFM call failed. + */ + public static int munmap(MemorySegment addr, long length) throws PlatformException{ if(hndMunmap == null){ var func = sym.find("munmap").get(); var desc = FunctionDescriptor.of( diff --git a/src/main/java/com/yasuenag/ffmasm/internal/linux/PerfJitDump.java b/src/main/java/com/yasuenag/ffmasm/internal/linux/PerfJitDump.java new file mode 100644 index 0000000..7cac90f --- /dev/null +++ b/src/main/java/com/yasuenag/ffmasm/internal/linux/PerfJitDump.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2024, Yasumasa Suenaga + * + * This file is part of ffmasm. + * + * ffmasm is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ffmasm 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ffmasm. If not, see . + */ +package com.yasuenag.ffmasm.internal.linux; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemoryLayout; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.StructLayout; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.VarHandle; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import com.yasuenag.ffmasm.CodeSegment; +import com.yasuenag.ffmasm.JitDump; +import com.yasuenag.ffmasm.PlatformException; +import com.yasuenag.ffmasm.UnsupportedPlatformException; + + +/** + * Generate jitdump for perf tool on Linux. + * + * @author Yasumasa Suenaga + */ +public class PerfJitDump implements JitDump{ + + // These constants come from tools/perf/util/jitdump.h in Linux Kernel + private static final int JITHEADER_MAGIC = 0x4A695444; + private static final int JITHEADER_VERSION = 1; + private static final int JIT_CODE_LOAD = 0; + private static final int JIT_CODE_CLOSE = 3; + + // from bits/time.h + private static final int CLOCK_MONOTONIC = 1; + + private static final long PAGE_SIZE = 4096; + + + private static final MethodHandle mhGetTid; + private static final StructLayout structTimespec; + private static final VarHandle hndSec; + private static final VarHandle hndNSec; + private static final MethodHandle mhClockGettime; + + + private final FileChannel ch; + + private final int fd; + + private final MemorySegment jitdump; + + private long codeIndex; + + + static{ + var linker = Linker.nativeLinker(); + var lookup = linker.defaultLookup(); + FunctionDescriptor desc; + + desc = FunctionDescriptor.of(ValueLayout.JAVA_INT); + mhGetTid = linker.downcallHandle(lookup.find("gettid").get(), desc); + + structTimespec = MemoryLayout.structLayout( + ValueLayout.JAVA_LONG.withName("tv_sec"), + ValueLayout.JAVA_LONG.withName("tv_nsec") + ); + hndSec = structTimespec.varHandle(MemoryLayout.PathElement.groupElement("tv_sec")); + hndNSec = structTimespec.varHandle(MemoryLayout.PathElement.groupElement("tv_nsec")); + // __CLOCKID_T_TYPE is defined as __S32_TYPE in bits/typesizes.h + desc = FunctionDescriptor.of(ValueLayout.JAVA_INT, + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS); + mhClockGettime = linker.downcallHandle(lookup.find("clock_gettime").get(), desc); + } + + private static int getTid(){ + try{ + return (int)mhGetTid.invokeExact(); + } + catch(Throwable t){ + throw new RuntimeException("Exception happened at gettid() call.", t); + } + } + + private static long getTimestamp(){ + try(var arena = Arena.ofConfined()){ + var timespec = arena.allocate(structTimespec); + int _ = (int)mhClockGettime.invokeExact(CLOCK_MONOTONIC, timespec); + + return ((long)hndSec.get(timespec, 0L) * 1_000_000_000) + (long)hndNSec.get(timespec, 0L); + } + catch(Throwable t){ + throw new RuntimeException("Exception happened at gettid() call.", t); + } + } + + // from tools/perf/util/jitdump.h in Linux Kernel + // struct jitheader { + // uint32_t magic; /* characters "jItD" */ + // uint32_t version; /* header version */ + // uint32_t total_size; /* total size of header */ + // uint32_t elf_mach; /* elf mach target */ + // uint32_t pad1; /* reserved */ + // uint32_t pid; /* JIT process id */ + // uint64_t timestamp; /* timestamp */ + // uint64_t flags; /* flags */ + // }; + private void writeHeader() throws IOException{ + final int headerSize = 40; // sizeof(struct jitheader) + var buf = ByteBuffer.allocate(headerSize).order(ByteOrder.nativeOrder()); + + short elfMach = -1; + try(var chExe = FileChannel.open(Path.of("/proc/self/exe"), StandardOpenOption.READ)){ + var buf2 = ByteBuffer.allocate(2).order(ByteOrder.nativeOrder()); + chExe.read(buf2, 18); // id (16 bytes) + e_type (2 bytes) + buf2.flip(); + elfMach = buf2.getShort(); + } + + // magic + buf.putInt(JITHEADER_MAGIC); + // version + buf.putInt(JITHEADER_VERSION); + // total_size + buf.putInt(headerSize); + // elf_mach + buf.putInt(elfMach); + // pad1 + buf.putInt(0); + // pid + buf.putInt((int)ProcessHandle.current().pid()); + // timestamp + buf.putLong(getTimestamp()); + // flags + buf.putLong(0L); + + buf.flip(); + ch.write(buf); + } + + /** + * Constructor of PerfJitDump. + * This constructer creates dump file named with "jit-.dump" + * into specified directory. Top of page (1 page: 4096 bytes) + * will be mapped as a executable memory - it is mandatory for + * recording in perf tool. + * And also jitdump header will be written at this time. + * + * @param dir Path to base directory to dump. + */ + public PerfJitDump(Path dir) throws UnsupportedPlatformException, PlatformException, IOException{ + // According to jit_detect() in tools/perf/util/jitdump.c in Linux Kernel, + // dump file should be named "jit-.dump". + var jitdumpPath = dir.resolve(String.format("jit-%d.dump", ProcessHandle.current().pid())); + ch = FileChannel.open(jitdumpPath, StandardOpenOption.CREATE_NEW, StandardOpenOption.READ, StandardOpenOption.WRITE); + + // find FD of jitdump file + int fdWork = -1; + try(var links = Files.newDirectoryStream(Path.of("/proc/self/fd"))){ + for(var link : links){ + try{ + if(Files.isSameFile(jitdumpPath, Files.readSymbolicLink(link))){ + fdWork = Integer.parseInt(link.getFileName().toString()); + break; + } + } + catch(NoSuchFileException e){ + // ignore + } + } + } + if(fdWork == -1){ + throw new IllegalStateException("FD of jitdump is not found."); + } + fd = fdWork; + + // from tools/perf/jvmti/jvmti_agent.c in Linux Kernel + jitdump = LinuxExecMemory.mmap(MemorySegment.NULL, PAGE_SIZE, LinuxExecMemory.PROT_READ | LinuxExecMemory.PROT_EXEC, LinuxExecMemory.MAP_PRIVATE, fd, 0); + codeIndex = 0; + + writeHeader(); + } + + // from tools/perf/util/jitdump.h in Linux Kernel + // struct jr_prefix { + // uint32_t id; + // uint32_t total_size; + // uint64_t timestamp; + // }; + // + // struct jr_code_load { + // struct jr_prefix p; + // + // uint32_t pid; + // uint32_t tid; + // uint64_t vma; + // uint64_t code_addr; + // uint64_t code_size; + // uint64_t code_index; + // }; + /** + * {@inheritDoc} + */ + @Override + public synchronized void writeFunction(CodeSegment.MethodInfo method){ + // sizeof(jr_code_load) == 56, null char of method name should be included. + final int totalSize = 56 + method.name().length() + 1 + method.size(); + var buf = ByteBuffer.allocate(totalSize).order(ByteOrder.nativeOrder()); + + // id + buf.putInt(JIT_CODE_LOAD); + // total_size + buf.putInt(totalSize); + // timestamp + buf.putLong(getTimestamp()); + // pid + buf.putInt((int)ProcessHandle.current().pid()); + // tid + buf.putInt(getTid()); + // vma + buf.putLong(method.address()); + // code_addr + buf.putLong(method.address()); + // code_size + buf.putLong(method.size()); + // code_index + buf.putLong(codeIndex++); + + // method name + buf.put(method.name().getBytes()); + buf.put((byte)0); // NUL + + // code + var seg = MemorySegment.ofAddress(method.address()).reinterpret(method.size()); + buf.put(seg.toArray(ValueLayout.JAVA_BYTE)); + + buf.flip(); + try{ + ch.write(buf); + } + catch(IOException e){ + throw new UncheckedIOException(e); + } + } + + // from tools/perf/util/jitdump.h in Linux Kernel + // struct jr_prefix { + // uint32_t id; + // uint32_t total_size; + // uint64_t timestamp; + // }; + // + // struct jr_code_close { + // struct jr_prefix p; + // }; + /** + * {@inheritDoc} + */ + @Override + public synchronized void close() throws Exception{ + final int headerSize = 16; // sizeof(jr_code_close) + var buf = ByteBuffer.allocate(headerSize).order(ByteOrder.nativeOrder()); + + // id + buf.putInt(JIT_CODE_CLOSE); + // total_size + buf.putInt(headerSize); + // timestamp + buf.putLong(getTimestamp()); + + buf.flip(); + ch.write(buf); + + LinuxExecMemory.munmap(jitdump, PAGE_SIZE); + ch.close(); + } + +} diff --git a/src/test/java/com/yasuenag/ffmasm/test/common/JitDumpTest.java b/src/test/java/com/yasuenag/ffmasm/test/common/JitDumpTest.java new file mode 100644 index 0000000..a5abd1c --- /dev/null +++ b/src/test/java/com/yasuenag/ffmasm/test/common/JitDumpTest.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2024, Yasumasa Suenaga + * + * This file is part of ffmasm. + * + * ffmasm is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ffmasm 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ffmasm. If not, see . + */ +package com.yasuenag.ffmasm.test.common; + +import java.io.File; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import com.yasuenag.ffmasm.CodeSegment; +import com.yasuenag.ffmasm.JitDump; +import com.yasuenag.ffmasm.UnsupportedPlatformException; +import com.yasuenag.ffmasm.internal.linux.PerfJitDump; + + +public class JitDumpTest{ + + private File getDumpFileInstance(){ + return new File(String.format("/tmp/jit-%d.dump", ProcessHandle.current().pid())); + } + + @Test + @EnabledOnOs(OS.LINUX) + public void testGetInstanceOnLinux(){ + try(var jitdump = JitDump.getInstance(Path.of("/tmp"))){ + Assertions.assertEquals(PerfJitDump.class, jitdump.getClass()); + } + catch(Exception e){ + Assertions.fail(e); + } + + var dumpfile = getDumpFileInstance(); + Assertions.assertTrue(dumpfile.exists()); + + // cleanup + dumpfile.delete(); + } + + @Test + @EnabledOnOs(OS.LINUX) + public void testMemoryPermissions(){ + var dumpfile = getDumpFileInstance(); + String line = null; + + try(var jitdump = JitDump.getInstance(Path.of("/tmp"))){ + try(var lines = Files.lines(Path.of("/proc/self/maps"))){ + line = lines.filter(s -> s.endsWith(dumpfile.getPath())) + .findFirst() + .get(); + } + } + catch(Exception e){ + Assertions.fail(e); + } + + Assertions.assertEquals("r-xp", line.split("\\s+")[1]); + + // cleanup + dumpfile.delete(); + } + + @Test + @EnabledOnOs(OS.LINUX) + public void testJitDumpHeader(){ + var dumpfile = getDumpFileInstance(); + + try(var jitdump = JitDump.getInstance(Path.of("/tmp"))){ + // Create jitdump file. Do nothing at here. + } + catch(Exception e){ + Assertions.fail(e); + } + + try(var ch = FileChannel.open(dumpfile.toPath(), StandardOpenOption.READ)){ + final int headerSize = 40; // sizeof(struct jitheader) + final int closeHeaderSize = 16; // sizeof(jr_code_close) + Assertions.assertEquals(headerSize + closeHeaderSize, ch.size()); + + var buf = ByteBuffer.allocate(headerSize) + .order(ByteOrder.nativeOrder()); + ch.read(buf); + buf.flip(); + + // magic + Assertions.assertEquals(0x4A695444, buf.getInt()); + // version + Assertions.assertEquals(1, buf.getInt()); + // total_size + Assertions.assertEquals(headerSize, buf.getInt()); + // elf_mach (skip) + buf.position(buf.position() + 4); + // pad1 + Assertions.assertEquals(0, buf.getInt()); + // pid + Assertions.assertEquals((int)ProcessHandle.current().pid(), buf.getInt()); + // timestamp (skip) + buf.position(buf.position() + 8); + // flag + Assertions.assertEquals(0L, buf.getLong()); + + buf = ByteBuffer.allocate(closeHeaderSize) + .order(ByteOrder.nativeOrder()); + ch.read(buf); + buf.flip(); + + // id + Assertions.assertEquals(3, buf.getInt()); + // total_size + Assertions.assertEquals(closeHeaderSize, buf.getInt()); + } + catch(Exception e){ + Assertions.fail(e); + } + + // cleanup + dumpfile.delete(); + } + + @Test + @EnabledOnOs(OS.LINUX) + public void testJitDumpFunctionEntry(){ + var dumpfile = getDumpFileInstance(); + var info = new CodeSegment.MethodInfo(null, "func", 0x1234, 0); + + try(var jitdump = JitDump.getInstance(Path.of("/tmp"))){ + jitdump.writeFunction(info); + } + catch(Exception e){ + Assertions.fail(e); + } + + try(var ch = FileChannel.open(dumpfile.toPath(), StandardOpenOption.READ)){ + final int fileHeaderSize = 40; // sizeof(struct jitheader) + final int functionEntrySize = 56 + info.name().length() + 1 + info.size(); // sizeof(jr_code_load) == 56, null char of method name should be included. + + var buf = ByteBuffer.allocate(functionEntrySize) + .order(ByteOrder.nativeOrder()); + + // Skip file header + ch.position(fileHeaderSize); + + // Read function entry + ch.read(buf); + buf.flip(); + + // id + Assertions.assertEquals(0, buf.getInt()); + // total_size + Assertions.assertEquals(functionEntrySize, buf.getInt()); + // timestamp (skip) + buf.position(buf.position() + 8); + // pid + Assertions.assertEquals((int)ProcessHandle.current().pid(), buf.getInt()); + // tid (skip) + buf.position(buf.position() + 4); + // vma + Assertions.assertEquals(0x1234L, buf.getLong()); + // code_addr + Assertions.assertEquals(0x1234L, buf.getLong()); + // code_size + Assertions.assertEquals(0L, buf.getLong()); + // code_index + Assertions.assertEquals(0L, buf.getLong()); + // function name + byte[] nameInBytes = new byte[info.name().length()]; + buf.get(nameInBytes); + Assertions.assertEquals("func", new String(nameInBytes)); + Assertions.assertEquals((byte)0, buf.get()); + } + catch(Exception e){ + Assertions.fail(e); + } + + // cleanup + dumpfile.delete(); + } + + @Test + @EnabledOnOs(OS.WINDOWS) + public void testGetInstanceOnWindows(){ + Assertions.assertThrows(UnsupportedPlatformException.class, () -> JitDump.getInstance(Path.of("C:\\tmp"))); + } + +}