blob: 4d936bd6cecb76cacb9ec328b0655dcec42a71b9 [file] [log] [blame] [edit]
// Copyright 2015 Marc-Antoine Ruel. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
// Package stack analyzes stack dump of Go processes and simplifies it.
//
// It is mostly useful on servers will large number of identical goroutines,
// making the crash dump harder to read than strictly necessary.
package stack
import (
"errors"
"fmt"
"net/url"
"os"
"sort"
"strings"
"unicode"
"unicode/utf8"
)
// Func is a function call in a goroutine stack trace.
type Func struct {
// Complete is the complete reference. It can be ambiguous in case where a
// path contains dots.
Complete string
// ImportPath is the directory name for this function reference, or "main" if
// it was in package main. The package name may not match.
ImportPath string
// DirName is the directory name containing the package in which the function
// is. Normally this matches the package name, but sometimes there's smartass
// folks that use a different directory name than the package name.
DirName string
// Name is the function name or fully quality method name.
Name string
// IsExported is true if the function is exported.
IsExported bool
// IsPkgMain is true if it is in the main package.
IsPkgMain bool
// Disallow initialization with unnamed parameters.
_ struct{}
}
// Init parses the raw function call line from a goroutine stack trace.
//
// Go stack traces print a mangled function call, this wrapper unmangle the
// string before printing and adds other filtering methods.
//
// The main caveat is that for calls in package main, the package import URL is
// left out.
func (f *Func) Init(raw string) error {
// Format can be:
// - gopkg.in/yaml%2ev2.(*Struct).Method (handling dots is tricky)
// - main.funcĀ·001 (go statements)
// - foo (C code)
//
// The function is optimized to reduce its memory usage.
endPkg := 0
if lastSlash := strings.LastIndexByte(raw, '/'); lastSlash != -1 {
// Cut the path elements.
r := strings.IndexByte(raw[lastSlash+1:], '.')
if r == -1 {
return errors.New("bad function reference: expected to have at least one dot")
}
endPkg = lastSlash + r + 1
} else {
// It's fine if there's no dot, it happens in C code in go1.4 and lower.
endPkg = strings.IndexByte(raw, '.')
}
// Only the path part is escaped.
var err error
if f.Complete, err = url.QueryUnescape(raw); err != nil {
return fmt.Errorf("bad function reference: %w", err)
}
// Update the index in the unescaped string.
endPkg += len(f.Complete) - len(raw)
if endPkg != -1 {
f.ImportPath = f.Complete[:endPkg]
}
f.Name = f.Complete[endPkg+1:]
if idx := strings.LastIndexByte(f.Name, ' '); idx > -1 {
cut := f.Name[:idx]
// TODO(go1.20): switch to strings.CutSuffix
const inGoroutineSuffix = " in goroutine"
if strings.HasSuffix(cut, inGoroutineSuffix) {
f.Name = strings.TrimSuffix(cut, inGoroutineSuffix)
}
}
f.DirName = f.ImportPath
if i := strings.LastIndexByte(f.DirName, '/'); i != -1 {
f.DirName = f.DirName[i+1:]
}
if f.ImportPath == "main" {
f.IsPkgMain = true
// Consider main.main to be exported.
if f.Name == "main" {
f.IsExported = true
}
} else {
parts := strings.Split(f.Name, ".")
r, _ := utf8.DecodeRuneInString(parts[len(parts)-1])
f.IsExported = unicode.ToUpper(r) == r
}
return nil
}
// String returns Complete.
func (f *Func) String() string {
return f.Complete
}
// Arg is an argument on a Call.
type Arg struct {
// IsAggregate is true if the argument is an aggregate type. If true, the
// argument does not contain a value itself, but contains a set of nested
// argument fields. If false, the argument contains a single scalar value.
IsAggregate bool
// The following are set if IsAggregate == false.
// Name is a pseudo name given to the argument.
Name string
// Value is the raw value as found in the stack trace
Value uint64
// IsPtr is true if we guess it's a pointer. It's only a guess, it can be
// easily confused by a bitmask.
IsPtr bool
// IsOffsetTooLarge is true if the argument's frame offset was too large,
// preventing the argument from being printed in the stack trace.
IsOffsetTooLarge bool
// IsInaccurate determines if Value is inaccurate. Stacks could have inaccurate values
// for arguments passed in registers. Go 1.18 prints a ? for these values.
IsInaccurate bool
// The following are set if IsAggregate == true.
// Fields are the fields/elements of aggregate-typed arguments.
Fields Args
// Disallow initialization with unnamed parameters.
_ struct{}
}
const zeroToNine = "0123456789"
// String prints the argument as the name if present, otherwise as the value.
func (a *Arg) String() string {
if a.Name != "" {
return a.Name
}
if a.IsOffsetTooLarge {
return "_"
}
if a.IsAggregate {
return "{" + a.Fields.String() + "}"
}
if a.Value < uint64(len(zeroToNine)) {
return zeroToNine[a.Value : a.Value+1]
}
return fmt.Sprintf("0x%x", a.Value)
}
const (
// The pointer floor can be below 1MiB (!) on Windows.
// Assumes all values are above 512KiB and positive are pointers; assuming
// that above half the memory is kernel memory.
//
// This is not always true but this should be good enough to help
// implementing AnyPointer.
pointerFloor = 512 * 1024
// Assume the stack was generated with the same bitness (32 vs 64) than the
// code processing it.
pointerCeiling = uint64((^uint(0)) >> 1)
)
// equal returns true only if both arguments are exactly equal.
func (a *Arg) equal(r *Arg) bool {
return a.similar(r, ExactFlags)
}
// similar returns true if the two Arg are equal or almost but not quite equal.
func (a *Arg) similar(r *Arg, similar Similarity) bool {
if a.IsAggregate != r.IsAggregate {
return false
}
if a.IsAggregate {
return a.Fields.similar(&r.Fields, similar)
}
switch similar {
case ExactFlags, ExactLines:
if a.Name != r.Name {
return false
}
if a.IsOffsetTooLarge != r.IsOffsetTooLarge {
return false
}
if a.IsPtr != r.IsPtr {
return false
}
return a.Value == r.Value
case AnyValue:
return true
case AnyPointer:
if a.IsOffsetTooLarge != r.IsOffsetTooLarge {
return false
}
if a.IsPtr != r.IsPtr {
return false
}
return a.IsPtr || a.Value == r.Value
default:
return false
}
}
// Args is a series of function call arguments.
type Args struct {
// Values is the arguments as shown on the stack trace. They are mangled via
// simplification.
Values []Arg
// Processed is the arguments generated from processing the source files. It
// can have a length lower than Values.
Processed []string
// Elided when set means there was a trailing ", ...".
Elided bool
// Disallow initialization with unnamed parameters.
_ struct{}
}
func (a *Args) String() string {
var v []string
if len(a.Processed) != 0 {
v = a.Processed
} else {
v = make([]string, 0, len(a.Values))
for _, item := range a.Values {
v = append(v, item.String())
}
}
if a.Elided {
v = append(v, "...")
}
return strings.Join(v, ", ")
}
// equal returns true only if both arguments are exactly equal.
func (a *Args) equal(r *Args) bool {
if a.Elided != r.Elided || len(a.Values) != len(r.Values) {
return false
}
for i, l := range a.Values {
if !l.equal(&r.Values[i]) {
return false
}
}
return true
}
// similar returns true if the two Args are equal or almost but not quite
// equal.
func (a *Args) similar(r *Args, similar Similarity) bool {
if a.Elided != r.Elided || len(a.Values) != len(r.Values) {
return false
}
for i, l := range a.Values {
if !l.similar(&r.Values[i], similar) {
return false
}
}
return true
}
// merge merges two similar Args, zapping out differences.
func (a *Args) merge(r *Args) Args {
out := Args{
Values: make([]Arg, len(a.Values)),
Elided: a.Elided,
}
for i, l := range a.Values {
rv := &r.Values[i]
if l.IsAggregate {
out.Values[i].IsAggregate = true
out.Values[i].Fields = l.Fields.merge(&rv.Fields)
} else if !l.equal(rv) {
out.Values[i].Name = "*"
out.Values[i].Value = l.Value
out.Values[i].IsPtr = l.IsPtr
} else {
out.Values[i] = l
}
}
return out
}
// walk traverses all non-aggregate arguments in the Args struct, calling the
// provided visitor function with each Arg.
func (a *Args) walk(visitor func(arg *Arg)) {
for i := range a.Values {
arg := &a.Values[i]
if arg.IsAggregate {
arg.Fields.walk(visitor)
} else {
visitor(arg)
}
}
}
// Location is the source location, if determined.
type Location int
const (
// LocationUnknown is the default value when Opts.GuessPaths was false.
LocationUnknown Location = iota
// GoMod is a go module, it is outside $GOPATH and is inside a directory
// containing a go.mod file. This is considered a local copy.
GoMod
// GOPATH is in $GOPATH/src. This is either a dependency fetched via
// GO111MODULE=off or intentionally fetched this way. There is no guaranteed
// that the local copy is pristine.
GOPATH
// GoPkg is in $GOPATH/pkg/mod. This is a dependency fetched via go module.
// It is considered to be an unmodified external dependency.
GoPkg
// Stdlib is when it is a Go standard library function. This includes the 'go
// test' generated main executable.
Stdlib
lastLocation
)
// Call is an item in the stack trace.
//
// All paths in this struct are in POSIX format, using "/" as path separator.
type Call struct {
// The following are initialized on the first line of the call stack.
// Func is the fully qualified function name (encoded).
Func Func
// Args is the call arguments.
Args Args
// The following are initialized on the second line of the call stack.
// RemoteSrcPath is the full path name of the source file as seen in the
// trace.
RemoteSrcPath string
// Line is the line number.
Line int
// SrcName is the base file name of the source file.
SrcName string
// DirSrc is one directory plus the file name of the source file. It is a
// subset of RemoteSrcPath.
DirSrc string
// The following are only set if Opts.GuessPaths was set.
// LocalSrcPath is the full path name of the source file as seen in the host,
// if found.
LocalSrcPath string
// RelSrcPath is the relative path to GOROOT, GOPATH or LocalGoMods.
RelSrcPath string
// ImportPath is the fully qualified import path as found on disk (when
// Opts.GuessPaths was set). Defaults to Func.ImportPath otherwise.
//
// In the case of package "main", it returns the underlying path to the main
// package instead of "main" if Opts.GuessPaths was set.
ImportPath string
// Location is the source location, if determined.
Location Location
// Disallow initialization with unnamed parameters.
_ struct{}
}
// Init initializes RemoteSrcPath, SrcName, DirName and Line.
//
// For test main, it initializes Location only with Stdlib.
//
// It does its best educated guess for ImportPath.
func (c *Call) init(srcPath string, line int) {
c.Line = line
if srcPath != "" {
c.RemoteSrcPath = srcPath
if i := strings.LastIndexByte(c.RemoteSrcPath, '/'); i != -1 {
c.SrcName = c.RemoteSrcPath[i+1:]
if i = strings.LastIndexByte(c.RemoteSrcPath[:i], '/'); i != -1 {
c.DirSrc = c.RemoteSrcPath[i+1:]
}
}
if c.DirSrc == testMainSrc {
// Consider _test/_testmain.go as stdlib since it's injected by "go test".
c.Location = Stdlib
}
}
c.ImportPath = c.Func.ImportPath
}
const testMainSrc = "_test" + string(os.PathSeparator) + "_testmain.go"
// updateLocations initializes LocalSrcPath, RelSrcPath, Location and ImportPath.
//
// goroot, localgoroot, localgomod, gomodImportPath and gopaths are expected to
// be in "/" format even on Windows. They must not have a trailing "/".
//
// Returns true if a match was found.
func (c *Call) updateLocations(goroot, localgoroot string, localgomods, gopaths map[string]string) bool {
// TODO(maruel): Reduce memory allocations.
if c.RemoteSrcPath == "" {
return false
}
// Check GOROOT first.
if goroot != "" {
if prefix := goroot + "/src/"; strings.HasPrefix(c.RemoteSrcPath, prefix) {
// Replace remote GOROOT with local GOROOT.
c.RelSrcPath = c.RemoteSrcPath[len(prefix):]
c.LocalSrcPath = pathJoin(localgoroot, "src", c.RelSrcPath)
if i := strings.LastIndexByte(c.RelSrcPath, '/'); i != -1 {
c.ImportPath = c.RelSrcPath[:i]
}
if c.Location == LocationUnknown {
c.Location = Stdlib
}
return true
}
}
// Check GOPATH.
// TODO(maruel): Sort for deterministic behavior?
for prefix, dest := range gopaths {
if p := prefix + "/src/"; strings.HasPrefix(c.RemoteSrcPath, p) {
c.RelSrcPath = c.RemoteSrcPath[len(p):]
c.LocalSrcPath = pathJoin(dest, "src", c.RelSrcPath)
if i := strings.LastIndexByte(c.RelSrcPath, '/'); i != -1 {
c.ImportPath = c.RelSrcPath[:i]
}
if c.Location == LocationUnknown {
c.Location = GOPATH
}
return true
}
// For modules, the path has to be altered, as it contains the version.
if p := prefix + "/pkg/mod/"; strings.HasPrefix(c.RemoteSrcPath, p) {
c.RelSrcPath = c.RemoteSrcPath[len(p):]
c.LocalSrcPath = pathJoin(dest, "pkg/mod", c.RelSrcPath)
if i := strings.LastIndexByte(c.RelSrcPath, '/'); i != -1 {
c.ImportPath = c.RelSrcPath[:i]
}
if c.Location == LocationUnknown {
c.Location = GoPkg
}
return true
}
}
// Check Go modules.
// Go module path detection only works with stack traces created on the local
// file system.
for prefix, pkg := range localgomods {
if strings.HasPrefix(c.RemoteSrcPath, prefix+"/") {
c.RelSrcPath = c.RemoteSrcPath[len(prefix)+1:]
c.LocalSrcPath = c.RemoteSrcPath
if i := strings.LastIndexByte(c.RelSrcPath, '/'); i != -1 {
c.ImportPath = pkg + "/" + c.RelSrcPath[:i]
} else {
c.ImportPath = pkg
}
if c.Location == LocationUnknown {
c.Location = GoMod
}
return true
}
}
// Maybe the path is just absolute and exists?
return false
}
// equal returns true only if both calls are exactly equal.
func (c *Call) equal(r *Call) bool {
return c.Line == r.Line && c.Func.Complete == r.Func.Complete && c.RemoteSrcPath == r.RemoteSrcPath && c.Args.equal(&r.Args)
}
// similar returns true if the two Call are equal or almost but not quite
// equal.
func (c *Call) similar(r *Call, similar Similarity) bool {
return c.Line == r.Line && c.Func.Complete == r.Func.Complete && c.RemoteSrcPath == r.RemoteSrcPath && c.Args.similar(&r.Args, similar)
}
// merge merges two similar Call, zapping out differences.
func (c *Call) merge(r *Call) Call {
return Call{
Func: c.Func,
Args: c.Args.merge(&r.Args),
RemoteSrcPath: c.RemoteSrcPath,
Line: c.Line,
SrcName: c.SrcName,
DirSrc: c.DirSrc,
LocalSrcPath: c.LocalSrcPath,
RelSrcPath: c.RelSrcPath,
ImportPath: c.ImportPath,
Location: c.Location,
}
}
// Stack is a call stack.
type Stack struct {
// Calls is the call stack. First is original function, last is leaf
// function.
Calls []Call
// Elided is set when there's >100 items in Stack, currently hardcoded in
// package runtime.
Elided bool
// Disallow initialization with unnamed parameters.
_ struct{}
}
// equal returns true on if both call stacks are exactly equal.
func (s *Stack) equal(r *Stack) bool {
if len(s.Calls) != len(r.Calls) || s.Elided != r.Elided {
return false
}
for i := range s.Calls {
if !s.Calls[i].equal(&r.Calls[i]) {
return false
}
}
return true
}
// similar returns true if the two Stack are equal or almost but not quite
// equal.
func (s *Stack) similar(r *Stack, similar Similarity) bool {
if len(s.Calls) != len(r.Calls) || s.Elided != r.Elided {
return false
}
for i := range s.Calls {
if !s.Calls[i].similar(&r.Calls[i], similar) {
return false
}
}
return true
}
// merge merges two similar Stack, zapping out differences.
func (s *Stack) merge(r *Stack) *Stack {
// Assumes similar stacks have the same length.
out := &Stack{
Calls: make([]Call, len(s.Calls)),
Elided: s.Elided,
}
for i := range s.Calls {
out.Calls[i] = s.Calls[i].merge(&r.Calls[i])
}
return out
}
// less compares two Stack, where the ones that are less are more
// important, so they come up front.
//
// A Stack with more private functions is 'less' so it is at the top.
// Inversely, a Stack with only public functions is 'more' so it is at the
// bottom.
func (s *Stack) less(r *Stack) bool {
lLoc := [lastLocation]int{}
rLoc := [lastLocation]int{}
lMain := 0
rMain := 0
for _, c := range s.Calls {
lLoc[c.Location]++
if c.Func.IsPkgMain {
lMain++
}
}
for _, s := range r.Calls {
rLoc[s.Location]++
if s.Func.IsPkgMain {
rMain++
}
}
if lMain > rMain {
return true
}
if lMain < rMain {
return false
}
for i := 1; i < int(lastLocation); i++ {
if lLoc[i] > rLoc[i] {
return true
}
if lLoc[i] < rLoc[i] {
return false
}
}
// Check unknown code type last.
if lLoc[LocationUnknown] > rLoc[LocationUnknown] {
return true
}
if lLoc[LocationUnknown] < rLoc[LocationUnknown] {
return false
}
// Stack lengths are the same and they are mostly of the same kind of location.
for x := range s.Calls {
if s.Calls[x].Func.Complete < r.Calls[x].Func.Complete {
return true
}
if s.Calls[x].Func.Complete > r.Calls[x].Func.Complete {
return false
}
if s.Calls[x].DirSrc < r.Calls[x].DirSrc {
return true
}
if s.Calls[x].DirSrc > r.Calls[x].DirSrc {
return false
}
if s.Calls[x].Line < r.Calls[x].Line {
return true
}
if s.Calls[x].Line > r.Calls[x].Line {
return false
}
}
// Stacks are the same.
return false
}
// updateLocations calls updateLocations on each call frame and returns true if
// they were all resolved.
func (s *Stack) updateLocations(goroot, localgoroot string, localgomods, gopaths map[string]string) bool {
// If there were none, it was "resolved".
r := true
for i := range s.Calls {
r = s.Calls[i].updateLocations(goroot, localgoroot, localgomods, gopaths) && r
}
return r
}
// Signature represents the signature of one or multiple goroutines.
//
// It is effectively the stack trace plus the goroutine internal bits, like
// it's state, if it is thread locked, which call site created this goroutine,
// etc.
type Signature struct {
// State is the goroutine state at the time of the snapshot.
//
// Use git grep 'gopark(|unlock)\(' to find them all plus everything listed
// in runtime/traceback.go. Valid values includes:
// - chan send, chan receive, select
// - finalizer wait, mark wait (idle),
// - Concurrent GC wait, GC sweep wait, force gc (idle)
// - IO wait, panicwait
// - semacquire, semarelease
// - sleep, timer goroutine (idle)
// - trace reader (blocked)
// Stuck cases:
// - chan send (nil chan), chan receive (nil chan), select (no cases)
// Runnable states:
// - idle, runnable, running, syscall, waiting, dead, enqueue, copystack,
// Scan states:
// - scan, scanrunnable, scanrunning, scansyscall, scanwaiting, scandead,
// scanenqueue
//
// When running under the race detector, the values are 'running' or
// 'finished'.
State string
// CreatedBy is the call stack that created this goroutine, if applicable.
//
// Normally, the stack is a single Call.
//
// When the race detector is enabled, a full stack snapshot is available.
CreatedBy Stack
// SleepMin is the wait time in minutes, if applicable.
//
// Not set when running under the race detector.
SleepMin int
// SleepMax is the wait time in minutes, if applicable.
//
// Not set when running under the race detector.
SleepMax int
// Stack is the call stack.
Stack Stack
// Locked is set if the goroutine was locked to an OS thread.
//
// Not set when running under the race detector.
Locked bool
// Disallow initialization with unnamed parameters.
_ struct{}
}
// equal returns true only if both signatures are exactly equal.
func (s *Signature) equal(r *Signature) bool {
if s.State != r.State || !s.CreatedBy.equal(&r.CreatedBy) || s.Locked != r.Locked || s.SleepMin != r.SleepMin || s.SleepMax != r.SleepMax {
return false
}
return s.Stack.equal(&r.Stack)
}
// similar returns true if the two Signature are equal or almost but not quite
// equal.
func (s *Signature) similar(r *Signature, similar Similarity) bool {
if s.State != r.State || !s.CreatedBy.similar(&r.CreatedBy, similar) {
return false
}
if similar == ExactFlags && s.Locked != r.Locked {
return false
}
return s.Stack.similar(&r.Stack, similar)
}
// merge merges two similar Signature, zapping out differences.
func (s *Signature) merge(r *Signature) *Signature {
min := s.SleepMin
if r.SleepMin < min {
min = r.SleepMin
}
max := s.SleepMax
if r.SleepMax > max {
max = r.SleepMax
}
return &Signature{
State: s.State, // Drop right side.
CreatedBy: s.CreatedBy, // Drop right side.
SleepMin: min,
SleepMax: max,
Stack: *s.Stack.merge(&r.Stack),
Locked: s.Locked || r.Locked, // TODO(maruel): This is weirdo.
}
}
// less compares two Signature, where the ones that are less are more
// important, so they come up front. A Signature with more private functions is
// 'less' so it is at the top. Inversely, a Signature with only public
// functions is 'more' so it is at the bottom.
func (s *Signature) less(r *Signature) bool {
if s.Stack.less(&r.Stack) {
return true
}
if r.Stack.less(&s.Stack) {
return false
}
if s.Locked && !r.Locked {
return true
}
if r.Locked && !s.Locked {
return false
}
if s.State < r.State {
return true
}
if s.State > r.State {
return false
}
return false
}
// SleepString returns a string "N-M minutes" if the goroutine(s) slept for a
// long time.
//
// Returns an empty string otherwise.
func (s *Signature) SleepString() string {
if s.SleepMax == 0 {
return ""
}
if s.SleepMin != s.SleepMax {
return fmt.Sprintf("%d~%d minutes", s.SleepMin, s.SleepMax)
}
return fmt.Sprintf("%d minutes", s.SleepMax)
}
// updateLocations calls updateLocations on both CreatedBy and Stack and
// returns true if they were both resolved.
func (s *Signature) updateLocations(goroot, localgoroot string, localgomods, gopaths map[string]string) bool {
r := s.CreatedBy.updateLocations(goroot, localgoroot, localgomods, gopaths)
r = s.Stack.updateLocations(goroot, localgoroot, localgomods, gopaths) && r
return r
}
// Goroutine represents the state of one goroutine, including the stack trace.
type Goroutine struct {
// Signature is the stack trace, internal bits, state, which call site
// created it, etc.
Signature
// ID is the goroutine id.
ID int
// First is the goroutine first printed, normally the one that crashed.
First bool
// RaceWrite is true if a race condition was detected, and this goroutine was
// race on a write operation, otherwise it was a read.
RaceWrite bool
// RaceAddr is set to the address when a data race condition was detected.
// Otherwise it is 0.
RaceAddr uint64
// Disallow initialization with unnamed parameters.
_ struct{}
}
// Private stuff.
// nameArguments is a post-processing step where Args are 'named' with numbers.
func nameArguments(goroutines []*Goroutine) {
// Set a name for any pointer occurring more than once.
type object struct {
args []*Arg
inPrimary bool
}
objects := map[uint64]object{}
// Enumerate all the arguments.
primary := true
visit := func(arg *Arg) {
if arg.IsPtr {
objects[arg.Value] = object{
args: append(objects[arg.Value].args, arg),
inPrimary: objects[arg.Value].inPrimary || primary,
}
}
}
for i, g := range goroutines {
primary = i == 0
for _, c := range g.Stack.Calls {
c.Args.walk(visit)
}
// CreatedBy.Args is never set.
}
order := make(uint64Slice, 0, len(objects)/2)
for k, obj := range objects {
if len(obj.args) > 1 && obj.inPrimary {
order = append(order, k)
}
}
sort.Sort(order)
nextID := 1
for _, k := range order {
for _, arg := range objects[k].args {
arg.Name = fmt.Sprintf("#%d", nextID)
}
nextID++
}
// Now do the rest. This is done so the output is deterministic.
order = make(uint64Slice, 0, len(objects))
for k := range objects {
order = append(order, k)
}
sort.Sort(order)
for _, k := range order {
// Process the remaining pointers, they were not referenced by primary
// thread so will have higher IDs.
if objects[k].inPrimary {
continue
}
for _, arg := range objects[k].args {
arg.Name = fmt.Sprintf("#%d", nextID)
}
nextID++
}
}
func pathJoin(s ...string) string {
return strings.Join(s, "/")
}
type uint64Slice []uint64
func (a uint64Slice) Len() int { return len(a) }
func (a uint64Slice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a uint64Slice) Less(i, j int) bool { return a[i] < a[j] }