339 lines
10 KiB
Go
339 lines
10 KiB
Go
// Package hsperfdata create data: 2018-06-12
|
|
// attention: The newest java HotSpot virtual machine performance data structures was V2 when I wrote this code, so these data structures may
|
|
// change from release to release, so this parser code only support JVM performance data V2. If there is new version, please open a issue
|
|
// or pull request, thx.
|
|
package hsperfdata
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// type code http://openjdk.java.net/groups/serviceability/jvmstat/sun/jvmstat/perfdata/monitor/v2_0/TypeCode.html
|
|
// source code https://github.com/dmlloyd/openjdk/blob/jdk/jdk/src/jdk.internal.jvmstat/share/classes/sun/jvmstat/perfdata/monitor/v2_0/TypeCode.java
|
|
// actually, we use only 'B' and 'J' in HotSpot performance data V2
|
|
const (
|
|
tBoolean = 'Z'
|
|
tChar = 'C'
|
|
tFloat = 'F'
|
|
tDouble = 'D'
|
|
tByte = 'B'
|
|
tShort = 'S'
|
|
tInt = 'I'
|
|
tLong = 'J'
|
|
tObject = 'L'
|
|
tArray = '['
|
|
tVoid = 'V'
|
|
)
|
|
|
|
// variability attribute https://github.com/dmlloyd/openjdk/blob/jdk/jdk/src/jdk.internal.jvmstat/share/classes/sun/jvmstat/monitor/Variability.java
|
|
const (
|
|
vInvalid = iota
|
|
vConstant
|
|
vMonotonic
|
|
vVariable
|
|
)
|
|
|
|
// unit of measure attribute https://github.com/dmlloyd/openjdk/blob/jdk/jdk/src/jdk.internal.jvmstat/share/classes/sun/jvmstat/monitor/Units.java
|
|
const (
|
|
uInvalid = iota
|
|
uNone
|
|
uBytes
|
|
uTicks
|
|
uEvents
|
|
uString
|
|
uHertz
|
|
)
|
|
|
|
// perfdataHeader http://openjdk.java.net/groups/serviceability/jvmstat/sun/jvmstat/perfdata/monitor/AbstractPerfDataBufferPrologue.html
|
|
// source code https://github.com/dmlloyd/openjdk/blob/jdk/jdk/src/jdk.internal.jvmstat/share/classes/sun/jvmstat/perfdata/monitor/AbstractPerfDataBufferPrologue.java
|
|
type perfdataHeader struct {
|
|
Magic uint32 // magic number - 0xcafec0c0
|
|
ByteOrder byte // big_endian == 0, little_endian == 1
|
|
Major byte // major version numbers
|
|
Minor byte // minor version numbers
|
|
// ReservedByte byte // used as Accessible flag at performance data V2
|
|
}
|
|
|
|
// prologue http://openjdk.java.net/groups/serviceability/jvmstat/sun/jvmstat/perfdata/monitor/v2_0/PerfDataBufferPrologue.html
|
|
// source code https://github.com/dmlloyd/openjdk/blob/jdk/jdk/src/jdk.internal.jvmstat/share/classes/sun/jvmstat/perfdata/monitor/v2_0/PerfDataBufferPrologue.java
|
|
type bufferPrologueV2 struct {
|
|
Accessible byte // Accessible flag at performance data V2
|
|
Used int32 // number of PerfData memory bytes used
|
|
Overflow int32 // number of bytes of overflow
|
|
ModTimestamp int64 // time stamp of the last structural modification
|
|
EntryOffset int32 // offset of the first PerfDataEntry
|
|
NumEntries int32 // number of allocated PerfData entries
|
|
}
|
|
|
|
// entryHeader http://openjdk.java.net/groups/serviceability/jvmstat/sun/jvmstat/perfdata/monitor/v2_0/PerfDataBuffer.html
|
|
// source code https://github.com/dmlloyd/openjdk/blob/jdk/jdk/src/jdk.internal.jvmstat/share/classes/sun/jvmstat/perfdata/monitor/v2_0/PerfDataBuffer.java
|
|
type entryHeader struct {
|
|
EntryLength int32 // entry length in bytes
|
|
NameOffset int32 // offset to entry name, relative to start of entry
|
|
VectorLength int32 // length of the vector. If 0, then scalar.
|
|
DataType byte // JNI field descriptor type
|
|
Flags byte // miscellaneous attribute flags 0x01 - supported
|
|
DataUnits byte // unit of measure attribute
|
|
DataVar byte // variability attribute
|
|
DataOffset int32 // offset to data item, relative to start of entry.
|
|
}
|
|
|
|
// PerfDataPath returns the path to the hsperfdata file for a given pid,
|
|
// it searches in all hsperfdata user directories (using a glob mattern),
|
|
// pid are assumed to be unique regardless of username.
|
|
func PerfDataPath(pid string) (string, error) {
|
|
perfGlob := filepath.Join(os.TempDir(), "hsperfdata_*", pid)
|
|
perfFiles, err := filepath.Glob(perfGlob)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(perfFiles) < 1 {
|
|
return "", fmt.Errorf("No hsperfdata file found for pid: %s", pid)
|
|
}
|
|
|
|
if len(perfFiles) > 1 {
|
|
return "", fmt.Errorf("More than one hsperfdata file found for pid: %s, this is not normal", pid)
|
|
}
|
|
|
|
filePath := perfFiles[0]
|
|
|
|
return filePath, nil
|
|
}
|
|
|
|
// PerfDataPaths returns a map(pid: dataPath) by the given pids
|
|
func PerfDataPaths(pids []string) (map[string]string, error) {
|
|
filePaths := make(map[string]string)
|
|
for _, pid := range pids {
|
|
filePath, err := PerfDataPath(pid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
filePaths[pid] = filePath
|
|
}
|
|
|
|
return filePaths, nil
|
|
}
|
|
|
|
// UserPerfDataPaths returns all the java process hsperfdata path belongs to
|
|
// the given user
|
|
func UserPerfDataPaths(user string) (map[string]string, error) {
|
|
dir := filepath.Join(os.TempDir(), "hsperfdata_"+user)
|
|
files, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filePaths := make(map[string]string)
|
|
for _, f := range files {
|
|
if _, err := strconv.Atoi(f.Name()); err == nil {
|
|
filePaths[f.Name()] = filepath.Join(dir, f.Name())
|
|
}
|
|
}
|
|
|
|
return filePaths, nil
|
|
}
|
|
|
|
// CurrentUserPerfDataPaths returns all the java process hsperfdata path belongs to
|
|
// the current user
|
|
func CurrentUserPerfDataPaths() (map[string]string, error) {
|
|
var user string
|
|
if runtime.GOOS == "windows" {
|
|
user = os.Getenv("USERNAME")
|
|
} else {
|
|
user = os.Getenv("USER")
|
|
}
|
|
if user == "" {
|
|
return nil, fmt.Errorf("error: Environment variable USER not set, can not find current user")
|
|
}
|
|
|
|
return UserPerfDataPaths(user)
|
|
}
|
|
|
|
// AllPerfDataPaths returns all users' hsperfdata path
|
|
func AllPerfDataPaths() (map[string]string, error) {
|
|
dirsGlob := filepath.Join(os.TempDir(), "hsperfdata_*", "*")
|
|
paths, err := filepath.Glob(dirsGlob)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
filePaths := make(map[string]string)
|
|
for _, path := range paths {
|
|
pid := filepath.Base(path)
|
|
filePaths[pid] = path
|
|
}
|
|
|
|
return filePaths, nil
|
|
}
|
|
|
|
// DataPathsByProcessName get data paths by the given process name
|
|
func DataPathsByProcessName(processName string) (map[string]string, error) {
|
|
var out []byte
|
|
var err error
|
|
if runtime.GOOS == "windows" {
|
|
out, err = exec.Command("cmd", "/C", "tasklist /NH|findstr /i "+processName).Output()
|
|
} else {
|
|
out, err = exec.Command("sh", "-c", "ps -ef|grep -i "+processName+"|grep -v grep").Output()
|
|
}
|
|
if err != nil || len(out) == 0 {
|
|
return nil, errors.New(processName + " is not running.")
|
|
}
|
|
|
|
filePaths := make(map[string]string)
|
|
buffer := bytes.NewBuffer(out)
|
|
for {
|
|
line, err := buffer.ReadString('\n')
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return nil, err
|
|
}
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
fields := strings.Fields(line)
|
|
pid := fields[1]
|
|
filePath, err := PerfDataPath(pid)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
filePaths[pid] = filePath
|
|
}
|
|
return filePaths, nil
|
|
}
|
|
|
|
// removeNull remove trailing '\x00' byte of s
|
|
func removeNull(s []byte) []byte {
|
|
if i := bytes.IndexByte(s, '\x00'); i >= 0 {
|
|
return s[:i]
|
|
}
|
|
return s
|
|
}
|
|
|
|
// ReadPerfData parser hotspot performance data, and return a map represented entries' name and value,
|
|
// "ticks" are the unit of measurement of time in the Hotspot JVM.
|
|
// when the parserTime is true, tick time value will be parsered to a normal nanoseconds using
|
|
// the "sun.os.hrt.frequency" key in the hsperfdata.
|
|
func ReadPerfData(filepath string, parserTime bool) (map[string]interface{}, error) {
|
|
// read a snapshot into memory
|
|
data, err := os.ReadFile(filepath)
|
|
if err != nil {
|
|
if runtime.GOOS == "windows" {
|
|
// on windows, can not read the hsperfdata file when the java process is running, so we copy
|
|
// to a new file first, and read the file, then delete it.
|
|
_, err = exec.Command("powershell", "-c", "cp", filepath, filepath+"_").Output()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err = os.ReadFile(filepath + "_")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_ = os.Remove(filepath + "_")
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
buffer := bytes.NewReader(data)
|
|
|
|
header := perfdataHeader{}
|
|
{
|
|
err = binary.Read(buffer, binary.BigEndian, &header)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if header.Magic != 0xcafec0c0 {
|
|
return nil, fmt.Errorf("illegal magic %v", header.Magic)
|
|
}
|
|
// only support 2.0 perf data buffers.
|
|
if !(header.Major == 2 && header.Minor == 0) {
|
|
return nil, fmt.Errorf("unsupported version %v.%v", header.Major, header.Minor)
|
|
}
|
|
}
|
|
|
|
// file endian
|
|
var endian binary.ByteOrder
|
|
if header.ByteOrder == 0 {
|
|
endian = binary.BigEndian
|
|
} else {
|
|
endian = binary.LittleEndian
|
|
}
|
|
|
|
prologue := bufferPrologueV2{}
|
|
{
|
|
err = binary.Read(buffer, endian, &prologue)
|
|
if prologue.Accessible != 1 {
|
|
return nil, fmt.Errorf("not accessible %v", prologue.Accessible)
|
|
}
|
|
}
|
|
|
|
entryMap := make(map[string]interface{})
|
|
startOffset := prologue.EntryOffset
|
|
var frequency time.Duration // the length of a tick
|
|
unconvertedTickFields := make(map[string]int64)
|
|
|
|
for i := int32(0); i < prologue.NumEntries; i++ {
|
|
entry := entryHeader{}
|
|
buffer.Seek(int64(startOffset), 0)
|
|
err = binary.Read(buffer, endian, &entry)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Cannot read binary: %v", err)
|
|
}
|
|
|
|
nameStart := int(startOffset) + int(entry.NameOffset)
|
|
nameEnd := bytes.Index(data[nameStart:], []byte{'\x00'})
|
|
if nameEnd < 0 {
|
|
return nil, fmt.Errorf("invalid binary: %v", err)
|
|
}
|
|
name := string(data[nameStart : int(nameStart)+nameEnd])
|
|
|
|
dataStart := startOffset + entry.DataOffset
|
|
if entry.VectorLength == 0 {
|
|
if entry.DataType != tLong {
|
|
return nil, fmt.Errorf("Unexpected monitor type: %v", entry.DataType)
|
|
}
|
|
buffer.Seek(int64(dataStart), 0)
|
|
value := int64(0)
|
|
err = binary.Read(buffer, endian, &value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if parserTime && entry.DataUnits == uTicks {
|
|
unconvertedTickFields[name] = value
|
|
}
|
|
if name == "sun.os.hrt.frequency" {
|
|
frequency = time.Duration(time.Second.Nanoseconds() / value)
|
|
}
|
|
entryMap[name] = value
|
|
} else {
|
|
if entry.DataType != tByte || entry.DataUnits != uString || (entry.DataVar != vConstant && entry.DataVar != vVariable) {
|
|
return nil, fmt.Errorf("Unexpected vector monitor: DataType:%c,DataUnits:%v,DataVar:%v", entry.DataType, entry.DataUnits, entry.DataVar)
|
|
}
|
|
|
|
value := string(removeNull(data[dataStart : dataStart+entry.VectorLength]))
|
|
entryMap[name] = value
|
|
}
|
|
|
|
startOffset += entry.EntryLength
|
|
}
|
|
|
|
for name, value := range unconvertedTickFields {
|
|
value *= frequency.Nanoseconds()
|
|
entryMap[name] = value
|
|
}
|
|
return entryMap, nil
|
|
}
|