legacy_java_profile.go raw

   1  // Copyright 2014 Google Inc. All Rights Reserved.
   2  //
   3  // Licensed under the Apache License, Version 2.0 (the "License");
   4  // you may not use this file except in compliance with the License.
   5  // You may obtain a copy of the License at
   6  //
   7  //     http://www.apache.org/licenses/LICENSE-2.0
   8  //
   9  // Unless required by applicable law or agreed to in writing, software
  10  // distributed under the License is distributed on an "AS IS" BASIS,
  11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12  // See the License for the specific language governing permissions and
  13  // limitations under the License.
  14  
  15  // This file implements parsers to convert java legacy profiles into
  16  // the profile.proto format.
  17  
  18  package profile
  19  
  20  import (
  21  	"bytes"
  22  	"fmt"
  23  	"io"
  24  	"path/filepath"
  25  	"regexp"
  26  	"strconv"
  27  	"strings"
  28  )
  29  
  30  var (
  31  	attributeRx            = regexp.MustCompile(`([\w ]+)=([\w ]+)`)
  32  	javaSampleRx           = regexp.MustCompile(` *(\d+) +(\d+) +@ +([ x0-9a-f]*)`)
  33  	javaLocationRx         = regexp.MustCompile(`^\s*0x([[:xdigit:]]+)\s+(.*)\s*$`)
  34  	javaLocationFileLineRx = regexp.MustCompile(`^(.*)\s+\((.+):(-?[[:digit:]]+)\)$`)
  35  	javaLocationPathRx     = regexp.MustCompile(`^(.*)\s+\((.*)\)$`)
  36  )
  37  
  38  // javaCPUProfile returns a new Profile from profilez data.
  39  // b is the profile bytes after the header, period is the profiling
  40  // period, and parse is a function to parse 8-byte chunks from the
  41  // profile in its native endianness.
  42  func javaCPUProfile(b []byte, period int64, parse func(b []byte) (uint64, []byte)) (*Profile, error) {
  43  	p := &Profile{
  44  		Period:     period * 1000,
  45  		PeriodType: &ValueType{Type: "cpu", Unit: "nanoseconds"},
  46  		SampleType: []*ValueType{{Type: "samples", Unit: "count"}, {Type: "cpu", Unit: "nanoseconds"}},
  47  	}
  48  	var err error
  49  	var locs map[uint64]*Location
  50  	if b, locs, err = parseCPUSamples(b, parse, false, p); err != nil {
  51  		return nil, err
  52  	}
  53  
  54  	if err = parseJavaLocations(b, locs, p); err != nil {
  55  		return nil, err
  56  	}
  57  
  58  	// Strip out addresses for better merge.
  59  	if err = p.Aggregate(true, true, true, true, false, false); err != nil {
  60  		return nil, err
  61  	}
  62  
  63  	return p, nil
  64  }
  65  
  66  // parseJavaProfile returns a new profile from heapz or contentionz
  67  // data. b is the profile bytes after the header.
  68  func parseJavaProfile(b []byte) (*Profile, error) {
  69  	h := bytes.SplitAfterN(b, []byte("\n"), 2)
  70  	if len(h) < 2 {
  71  		return nil, errUnrecognized
  72  	}
  73  
  74  	p := &Profile{
  75  		PeriodType: &ValueType{},
  76  	}
  77  	header := string(bytes.TrimSpace(h[0]))
  78  
  79  	var err error
  80  	var pType string
  81  	switch header {
  82  	case "--- heapz 1 ---":
  83  		pType = "heap"
  84  	case "--- contentionz 1 ---":
  85  		pType = "contention"
  86  	default:
  87  		return nil, errUnrecognized
  88  	}
  89  
  90  	if b, err = parseJavaHeader(pType, h[1], p); err != nil {
  91  		return nil, err
  92  	}
  93  	var locs map[uint64]*Location
  94  	if b, locs, err = parseJavaSamples(pType, b, p); err != nil {
  95  		return nil, err
  96  	}
  97  	if err = parseJavaLocations(b, locs, p); err != nil {
  98  		return nil, err
  99  	}
 100  
 101  	// Strip out addresses for better merge.
 102  	if err = p.Aggregate(true, true, true, true, false, false); err != nil {
 103  		return nil, err
 104  	}
 105  
 106  	return p, nil
 107  }
 108  
 109  // parseJavaHeader parses the attribute section on a java profile and
 110  // populates a profile. Returns the remainder of the buffer after all
 111  // attributes.
 112  func parseJavaHeader(pType string, b []byte, p *Profile) ([]byte, error) {
 113  	nextNewLine := bytes.IndexByte(b, byte('\n'))
 114  	for nextNewLine != -1 {
 115  		line := string(bytes.TrimSpace(b[0:nextNewLine]))
 116  		if line != "" {
 117  			h := attributeRx.FindStringSubmatch(line)
 118  			if h == nil {
 119  				// Not a valid attribute, exit.
 120  				return b, nil
 121  			}
 122  
 123  			attribute, value := strings.TrimSpace(h[1]), strings.TrimSpace(h[2])
 124  			var err error
 125  			switch pType + "/" + attribute {
 126  			case "heap/format", "cpu/format", "contention/format":
 127  				if value != "java" {
 128  					return nil, errUnrecognized
 129  				}
 130  			case "heap/resolution":
 131  				p.SampleType = []*ValueType{
 132  					{Type: "inuse_objects", Unit: "count"},
 133  					{Type: "inuse_space", Unit: value},
 134  				}
 135  			case "contention/resolution":
 136  				p.SampleType = []*ValueType{
 137  					{Type: "contentions", Unit: "count"},
 138  					{Type: "delay", Unit: value},
 139  				}
 140  			case "contention/sampling period":
 141  				p.PeriodType = &ValueType{
 142  					Type: "contentions", Unit: "count",
 143  				}
 144  				if p.Period, err = strconv.ParseInt(value, 0, 64); err != nil {
 145  					return nil, fmt.Errorf("failed to parse attribute %s: %v", line, err)
 146  				}
 147  			case "contention/ms since reset":
 148  				millis, err := strconv.ParseInt(value, 0, 64)
 149  				if err != nil {
 150  					return nil, fmt.Errorf("failed to parse attribute %s: %v", line, err)
 151  				}
 152  				p.DurationNanos = millis * 1000 * 1000
 153  			default:
 154  				return nil, errUnrecognized
 155  			}
 156  		}
 157  		// Grab next line.
 158  		b = b[nextNewLine+1:]
 159  		nextNewLine = bytes.IndexByte(b, byte('\n'))
 160  	}
 161  	return b, nil
 162  }
 163  
 164  // parseJavaSamples parses the samples from a java profile and
 165  // populates the Samples in a profile. Returns the remainder of the
 166  // buffer after the samples.
 167  func parseJavaSamples(pType string, b []byte, p *Profile) ([]byte, map[uint64]*Location, error) {
 168  	nextNewLine := bytes.IndexByte(b, byte('\n'))
 169  	locs := make(map[uint64]*Location)
 170  	for nextNewLine != -1 {
 171  		line := string(bytes.TrimSpace(b[0:nextNewLine]))
 172  		if line != "" {
 173  			sample := javaSampleRx.FindStringSubmatch(line)
 174  			if sample == nil {
 175  				// Not a valid sample, exit.
 176  				return b, locs, nil
 177  			}
 178  
 179  			// Java profiles have data/fields inverted compared to other
 180  			// profile types.
 181  			var err error
 182  			value1, value2, value3 := sample[2], sample[1], sample[3]
 183  			addrs, err := parseHexAddresses(value3)
 184  			if err != nil {
 185  				return nil, nil, fmt.Errorf("malformed sample: %s: %v", line, err)
 186  			}
 187  
 188  			var sloc []*Location
 189  			for _, addr := range addrs {
 190  				loc := locs[addr]
 191  				if locs[addr] == nil {
 192  					loc = &Location{
 193  						Address: addr,
 194  					}
 195  					p.Location = append(p.Location, loc)
 196  					locs[addr] = loc
 197  				}
 198  				sloc = append(sloc, loc)
 199  			}
 200  			s := &Sample{
 201  				Value:    make([]int64, 2),
 202  				Location: sloc,
 203  			}
 204  
 205  			if s.Value[0], err = strconv.ParseInt(value1, 0, 64); err != nil {
 206  				return nil, nil, fmt.Errorf("parsing sample %s: %v", line, err)
 207  			}
 208  			if s.Value[1], err = strconv.ParseInt(value2, 0, 64); err != nil {
 209  				return nil, nil, fmt.Errorf("parsing sample %s: %v", line, err)
 210  			}
 211  
 212  			switch pType {
 213  			case "heap":
 214  				const javaHeapzSamplingRate = 524288 // 512K
 215  				if s.Value[0] == 0 {
 216  					return nil, nil, fmt.Errorf("parsing sample %s: second value must be non-zero", line)
 217  				}
 218  				s.NumLabel = map[string][]int64{"bytes": {s.Value[1] / s.Value[0]}}
 219  				s.Value[0], s.Value[1] = scaleHeapSample(s.Value[0], s.Value[1], javaHeapzSamplingRate)
 220  			case "contention":
 221  				if period := p.Period; period != 0 {
 222  					s.Value[0] = s.Value[0] * p.Period
 223  					s.Value[1] = s.Value[1] * p.Period
 224  				}
 225  			}
 226  			p.Sample = append(p.Sample, s)
 227  		}
 228  		// Grab next line.
 229  		b = b[nextNewLine+1:]
 230  		nextNewLine = bytes.IndexByte(b, byte('\n'))
 231  	}
 232  	return b, locs, nil
 233  }
 234  
 235  // parseJavaLocations parses the location information in a java
 236  // profile and populates the Locations in a profile. It uses the
 237  // location addresses from the profile as both the ID of each
 238  // location.
 239  func parseJavaLocations(b []byte, locs map[uint64]*Location, p *Profile) error {
 240  	r := bytes.NewBuffer(b)
 241  	fns := make(map[string]*Function)
 242  	for {
 243  		line, err := r.ReadString('\n')
 244  		if err != nil {
 245  			if err != io.EOF {
 246  				return err
 247  			}
 248  			if line == "" {
 249  				break
 250  			}
 251  		}
 252  
 253  		if line = strings.TrimSpace(line); line == "" {
 254  			continue
 255  		}
 256  
 257  		jloc := javaLocationRx.FindStringSubmatch(line)
 258  		if len(jloc) != 3 {
 259  			continue
 260  		}
 261  		addr, err := strconv.ParseUint(jloc[1], 16, 64)
 262  		if err != nil {
 263  			return fmt.Errorf("parsing sample %s: %v", line, err)
 264  		}
 265  		loc := locs[addr]
 266  		if loc == nil {
 267  			// Unused/unseen
 268  			continue
 269  		}
 270  		var lineFunc, lineFile string
 271  		var lineNo int64
 272  
 273  		if fileLine := javaLocationFileLineRx.FindStringSubmatch(jloc[2]); len(fileLine) == 4 {
 274  			// Found a line of the form: "function (file:line)"
 275  			lineFunc, lineFile = fileLine[1], fileLine[2]
 276  			if n, err := strconv.ParseInt(fileLine[3], 10, 64); err == nil && n > 0 {
 277  				lineNo = n
 278  			}
 279  		} else if filePath := javaLocationPathRx.FindStringSubmatch(jloc[2]); len(filePath) == 3 {
 280  			// If there's not a file:line, it's a shared library path.
 281  			// The path isn't interesting, so just give the .so.
 282  			lineFunc, lineFile = filePath[1], filepath.Base(filePath[2])
 283  		} else if strings.Contains(jloc[2], "generated stub/JIT") {
 284  			lineFunc = "STUB"
 285  		} else {
 286  			// Treat whole line as the function name. This is used by the
 287  			// java agent for internal states such as "GC" or "VM".
 288  			lineFunc = jloc[2]
 289  		}
 290  		fn := fns[lineFunc]
 291  
 292  		if fn == nil {
 293  			fn = &Function{
 294  				Name:       lineFunc,
 295  				SystemName: lineFunc,
 296  				Filename:   lineFile,
 297  			}
 298  			fns[lineFunc] = fn
 299  			p.Function = append(p.Function, fn)
 300  		}
 301  		loc.Line = []Line{
 302  			{
 303  				Function: fn,
 304  				Line:     lineNo,
 305  			},
 306  		}
 307  		loc.Address = 0
 308  	}
 309  
 310  	p.remapLocationIDs()
 311  	p.remapFunctionIDs()
 312  	p.remapMappingIDs()
 313  
 314  	return nil
 315  }
 316