Commit b8526271 authored by Kirill Smelkov's avatar Kirill Smelkov

X tracing moved -> go123

parent e3937d32
// Copyright (C) 2017 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
/*
Gotrace is a program to support and interact with go tracing subsystem.
Gotrace is a common entry to tracing and provides several subcommands:
gen generate code according to tracing annotations and imports
list lists tracepoints defined by a package
See package lab.nexedi.com/kirr/go123/tracing documentation on how to define
and use trace events in programs.
TODO automatically turn every trace:event into an USDT probe so that they can
be traced from outside of the process too.
See e.g. https://github.com/iovisor/bcc/issues/327 for context.
FIXME build tags not taken into account
*/
package main
import (
"bufio"
"crypto/sha1"
"flag"
"fmt"
"go/ast"
"go/build"
"go/parser"
"go/token"
"go/types"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"text/template"
"golang.org/x/tools/go/loader"
"lab.nexedi.com/kirr/go123/prog"
"lab.nexedi.com/kirr/go123/xerr"
)
// traceEvent represents 1 trace:event declaration
type traceEvent struct {
Pos token.Position
Pkgt *Package // package this trace event is part of
// declaration of function to signal the event
// the declaration is constructed on the fly via converting e.g.
//
// //trace:event traceConnRecv(c *Conn, msg Msg)
//
// into
//
// func traceConnRecv(c *Conn, msg Msg)
//
// when trace:event is parsed the func declaration is not added
// anywhere in the sources - just its AST + package is virtually
// constructed.
//
// See parseTraceEvent for details.
*ast.FuncDecl
}
// traceImport represents 1 trace:import directive
type traceImport struct {
Pos token.Position
PkgName string // "" if import name was not explicitly specified
PkgPath string
}
// traceImported represents 1 imported trace:event
type traceImported struct {
*traceEvent // imported event
ImportSpec *traceImport // imported via this spec
ImporterPkg *types.Package // from this package
ImportedAs map[string]string // in context where some packages are imported as named (pkgpath -> pkgname)
}
// Package represents tracing-related information about a package
type Package struct {
Pkgi *loader.PackageInfo // original non-augmented package
Eventv []*traceEvent // trace events this package defines
Importv []*traceImport // trace imports of other packages
// original package is augmented with tracing code
// information about tracing code is below:
traceFilev []*ast.File // files for added trace:event funcs
traceFset *token.FileSet // fset for ^^^
traceChecker *types.Checker // to typecheck ^^^
tracePkg *types.Package // original package augmented with ^^^
traceTypeInfo *types.Info // typeinfo for ^^^
}
// parseTraceEvent parses trace event definition into traceEvent.
//
// text is text argument after "//trace:event ".
func (p *Package) parseTraceEvent(srcfile *ast.File, pos token.Position, text string) (*traceEvent, error) {
posErr := func(format string, argv ...interface{}) error {
return fmt.Errorf("%v: "+format, append([]interface{}{pos}, argv...)...)
}
if !strings.HasPrefix(text, "trace") {
return nil, posErr("trace event must start with \"trace\"")
}
// prepare artificial package with trace event definition as func declaration
buf := &Buffer{}
buf.emit("package %s", p.Pkgi.Pkg.Name())
// add all imports from original source file
// so that inside it all looks like as if it was in original source context
buf.emit("\nimport (")
for _, imp := range srcfile.Imports {
impline := ""
if imp.Name != nil {
impline += imp.Name.Name + " "
}
impline += imp.Path.Value
buf.emit("\t%s", impline)
}
buf.emit(")")
// func itself
buf.emit("\nfunc " + text)
// now parse/typecheck
filename := fmt.Sprintf("%v:%v+trace:event %v", pos.Filename, pos.Line, text)
//println("---- 8< ----", filename)
//println(buf.String())
//println("---- 8< ----")
tf, err := parser.ParseFile(p.traceFset, filename, buf.String(), 0)
if err != nil {
return nil, err // already has pos' as prefix
}
p.traceFilev = append(p.traceFilev, tf)
// must be:
// GenDecl{IMPORT}
// FuncDecl
if len(tf.Decls) != 2 {
return nil, posErr("trace event must be func-like")
}
declf, ok := tf.Decls[1].(*ast.FuncDecl)
if !ok {
return nil, posErr("trace event must be func-like, not %v", tf.Decls[0])
}
// XXX ok to allow methods (declf.Recv != nil) ?
if declf.Type.Results != nil {
return nil, posErr("trace event must not return results")
}
// typecheck prepared file to get trace func argument types
// (type information lands into p.traceTypeInfo)
err = p.traceChecker.Files([]*ast.File{tf})
if err != nil {
return nil, err // already has pos' as prefix
}
return &traceEvent{Pos: pos, Pkgt: p, FuncDecl: declf}, nil
}
// parseTraceImport parses trace import directive into traceImport.
//
// text is text argument after "//trace:import ".
func (p *Package) parseTraceImport(pos token.Position, text string) (*traceImport, error) {
// //trace:import "path/to/pkg"
// //trace:import name "path/to/pkg"
if len(text) == 0 {
return nil, fmt.Errorf("%v: empty trace-import spec", pos)
}
pkgname := ""
pkgqpath := text
if !(text[0] == '"' || text[0] == '\'') {
textv := strings.SplitN(text, " ", 2)
if len(textv) != 2 {
return nil, fmt.Errorf("%v: invalid trace-import spec %v", pos, text)
}
pkgname = textv[0]
pkgqpath = textv[1]
}
// Unquote pkgqpath as regular import does
pkgpath, err := strconv.Unquote(pkgqpath)
if err != nil || pkgpath == "" || pkgpath[0] == '\'' {
return nil, fmt.Errorf("%v: invalid trace-import path %v", pos, pkgqpath)
}
// reject duplicate imports
for _, imported := range p.Importv {
if pkgpath == imported.PkgPath {
return nil, fmt.Errorf("%v: duplicate trace import of %v (previous at %v)", pos, pkgpath, imported.Pos)
}
}
return &traceImport{Pos: pos, PkgName: pkgname, PkgPath: pkgpath}, nil
}
// progImporter is types.Importer that imports packages from loaded loader.Program
type progImporter struct {
prog *loader.Program
}
func (pi *progImporter) Import(path string) (*types.Package, error) {
pkgi := pi.prog.Package(path)
if pkgi == nil {
return nil, fmt.Errorf("package %q not found", path)
}
return pkgi.Pkg, nil
}
// packageTrace returns tracing information about a package
func packageTrace(prog *loader.Program, pkgi *loader.PackageInfo) (*Package, error) {
// prepare Package with typechecker ready to typecheck trace files
// (to get trace func argument types)
tconf := &types.Config{
Importer: &progImporter{prog},
// to ignore traceXXX() calls from original package code
IgnoreFuncBodies: true,
// we take imports from original source file verbatim,
// but most of them probably won't be used.
DisableUnusedImportCheck: true,
}
// tfset := token.NewFileSet() // XXX ok to separate or use original package fset?
tfset := prog.Fset
tpkg := types.NewPackage(pkgi.Pkg.Path(), pkgi.Pkg.Name())
tinfo := &types.Info{Types: make(map[ast.Expr]types.TypeAndValue)}
p := &Package{
Pkgi: pkgi,
// XXX vvv do we need separate field for traceFset if it is = prog.Fset?
traceFset: tfset,
traceChecker: types.NewChecker(tconf, tfset, tpkg, tinfo),
tracePkg: tpkg,
traceTypeInfo: tinfo,
}
// preload original package files into tracing package
err := p.traceChecker.Files(p.Pkgi.Files)
if err != nil {
// must not happen
panic(fmt.Errorf("%v: error rechecking original package: %v", pkgi.Pkg.Path(), err))
}
// go through files of the original package and process //trace: directives
//
// FIXME we currently don't process cgo files as go/loader passes to us
// already preprocessed results with comments stripped, not original source.
// Maybe in some time it will be possible to have AST of original source:
// https://golang.org/issues/16623
for _, file := range pkgi.Files { // ast.File
for _, commgroup := range file.Comments { // ast.CommentGroup
for _, comment := range commgroup.List { // ast.Comment
pos := prog.Fset.Position(comment.Slash)
//fmt.Printf("%v %q\n", pos, comment.Text)
// only directives starting from beginning of line
if pos.Column != 1 {
continue
}
if !strings.HasPrefix(comment.Text, "//trace:") {
continue
}
textv := strings.SplitN(comment.Text, " ", 2)
if len(textv) != 2 {
return nil, fmt.Errorf("%v: invalid directive format", pos)
}
directive, arg := textv[0], textv[1]
switch directive {
case "//trace:event":
//fmt.Println("*", textv)
event, err := p.parseTraceEvent(file, pos, arg)
if err != nil {
return nil, err
}
// XXX needed here? - better append in parseTraceEvent
p.Eventv = append(p.Eventv, event)
case "//trace:import":
imported, err := p.parseTraceImport(pos, arg)
if err != nil {
return nil, err
}
// XXX needed here? - better append in parseTraceImport
p.Importv = append(p.Importv, imported)
default:
return nil, fmt.Errorf("%v: unknown tracing directive %q", pos, directive)
}
}
}
}
// events and imports go in canonical order
sort.Sort(byEventName(p.Eventv))
sort.Sort(byPkgPath(p.Importv))
return p, nil
}
// byEventName provides []*traceEvent ordering by event name
type byEventName []*traceEvent
func (v byEventName) Less(i, j int) bool { return v[i].Name.Name < v[j].Name.Name }
func (v byEventName) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
func (v byEventName) Len() int { return len(v) }
// byPkgPath provides []*traceImport ordering by package path
type byPkgPath []*traceImport
func (v byPkgPath) Less(i, j int) bool { return v[i].PkgPath < v[j].PkgPath }
func (v byPkgPath) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
func (v byPkgPath) Len() int { return len(v) }
// SplitTests splits package into main and test parts, each covering trace-related things accordingly
func (p *Package) SplitTests() (testPkg *Package) {
__ := *p
testPkg = &__
// relevant for tracing are only: .Eventv & .Importv
eventv := p.Eventv
importv := p.Importv
p.Eventv = nil
p.Importv = nil
testPkg.Eventv = nil
testPkg.Importv = nil
for _, e := range eventv {
if strings.HasSuffix(e.Pos.Filename, "_test.go") {
testPkg.Eventv = append(testPkg.Eventv, e)
} else {
p.Eventv = append(p.Eventv, e)
}
}
for _, i := range importv {
if strings.HasSuffix(i.Pos.Filename, "_test.go") {
testPkg.Importv = append(testPkg.Importv, i)
} else {
p.Importv = append(p.Importv, i)
}
}
return testPkg
}
// ----------------------------------------
// Argv returns comma-separated argument-list
func (te *traceEvent) Argv() string {
argv := []string{}
for _, field := range te.FuncDecl.Type.Params.List {
for _, name := range field.Names {
argv = append(argv, name.Name)
}
}
return strings.Join(argv, ", ")
}
// ArgvTyped returns argument list with types.
//
// types are qualified relative to original package
func (te *traceEvent) ArgvTyped() string {
return te.ArgvTypedRelativeTo(te.Pkgt.tracePkg, nil)
}
// ArgvTypedRelativeTo returns argument list with types qualified relative to specified package.
//
// importedAs specifies under which name a package was imported, if name was explicitly set
func (te *traceEvent) ArgvTypedRelativeTo(pkg *types.Package, importedAs map[string]string /*pkgpath -> pkgname*/) string {
argv := []string{}
// default qualifier - relative to original package
qf := func(p *types.Package) string {
// specified package - unqualified
if p == pkg {
return ""
}
// qualify as explicitly named
pkgname := importedAs[p.Path()]
if pkgname != "" {
return pkgname
}
// default qualification
return p.Name()
}
for _, field := range te.FuncDecl.Type.Params.List {
namev := []string{}
for _, name := range field.Names {
namev = append(namev, name.Name)
}
arg := strings.Join(namev, ", ")
typ := te.Pkgt.traceTypeInfo.Types[field.Type].Type
arg += " " + types.TypeString(typ, qf)
argv = append(argv, arg)
}
return strings.Join(argv, ", ")
}
// NeedPkgv returns packages that are needed for argument types
func (te *traceEvent) NeedPkgv() []string {
pkgset := StrSet{ /*pkgpath*/ }
qf := func(pkg *types.Package) string {
// if we are called - pkg is used
pkgset.Add(pkg.Path())
return "" // don't care
}
for _, field := range te.FuncDecl.Type.Params.List {
typ := te.Pkgt.traceTypeInfo.Types[field.Type].Type
_ = types.TypeString(typ, qf)
}
return pkgset.Itemv()
}
// ImportSpec returns string representation of import spec
func (ti *traceImport) ImportSpec() string {
t := ti.PkgName
if t != "" {
t += " "
}
t += fmt.Sprintf("%q", ti.PkgPath)
return t
}
// traceEventCodeTmpl is code template generated for one trace event
var traceEventCodeTmpl = template.Must(template.New("traceevent").Parse(`
// traceevent: {{.Name}}({{.ArgvTyped}})
{{/* probe type for this trace event */ -}}
type _t_{{.Name}} struct {
tracing.Probe
probefunc func({{.ArgvTyped}})
}
{{/* list of probes attached (nil if nothing) */ -}}
var _{{.Name}} *_t_{{.Name}}
{{/* function which event producer calls to notify about the event
*
* after https://github.com/golang/go/issues/19348 is done this separate
* checking function will be inlined and tracepoint won't cost a function
* call when it is disabled */ -}}
func {{.Name}}({{.ArgvTyped}}) {
if _{{.Name}} != nil {
_{{.Name}}_run({{.Argv}})
}
}
{{/* function to notify attached probes */ -}}
func _{{.Name}}_run({{.ArgvTyped}}) {
for p := _{{.Name}}; p != nil; p = (*_t_{{.Name}})(unsafe.Pointer(p.Next())) {
p.probefunc({{.Argv}})
}
}
{{/* function to attach a probe to tracepoint */ -}}
func {{.Name}}_Attach(pg *tracing.ProbeGroup, probe func({{.ArgvTyped}})) *tracing.Probe {
p := _t_{{.Name}}{probefunc: probe}
tracing.AttachProbe(pg, (**tracing.Probe)(unsafe.Pointer(&_{{.Name}})), &p.Probe)
return &p.Probe
}
`))
// traceEventImportTmpl is code template generated for importing one trace event
var traceEventImportTmpl = template.Must(template.New("traceimport").Parse(`
{{/* function to attach a probe to tracepoint imported via go:linkname */ -}}
//go:linkname {{.ImportSpec.PkgName}}_{{.Name}}_Attach {{.ImportSpec.PkgPath}}.{{.Name}}_Attach
func {{.ImportSpec.PkgName}}_{{.Name}}_Attach(*tracing.ProbeGroup, func({{.ArgvTypedRelativeTo .ImporterPkg .ImportedAs}})) *tracing.Probe
`))
// traceEventImportCheckTmpl is code template generated to check consistency with one imported package
var traceEventImportCheckTmpl = template.Must(template.New("traceimportcheck").Parse(`
{{/* linking will fail if trace import code becomes out of sync wrt imported package */ -}}
// rerun "gotrace gen" if you see link failure ↓↓↓
//go:linkname {{.ImportSpec.PkgName}}_trace_exporthash {{.ImportSpec.PkgPath}}._trace_exporthash_{{.ExportHash}}
func {{.ImportSpec.PkgName}}_trace_exporthash()
func init() { {{.ImportSpec.PkgName}}_trace_exporthash() }
`))
// magic begins all files generated by gotrace
const magic = "// Code generated by lab.nexedi.com/kirr/go123/tracing/cmd/gotrace; DO NOT EDIT.\n"
// checkCanWrite checks whether it is safe to write to file at path.
//
// it is safe to write when either
// - the file does not exist, or
// - it exits but was previously generated by us
func checkCanWrite(path string) error {
f, err := os.Open(path)
if e, ok := err.(*os.PathError); ok && os.IsNotExist(e.Err) {
return nil
}
defer f.Close()
bf := bufio.NewReader(f)
headline, err := bf.ReadString('\n')
if err != nil || headline != magic {
return fmt.Errorf("refusing to make output: %v exists but was not generated by gotrace", path)
}
return nil
}
// writeFile writes data to a file at path after checking it is safe to write there
func writeFile(path string, data []byte) error {
err := checkCanWrite(path)
if err != nil {
return err
}
return ioutil.WriteFile(path, data, 0666)
}
// removeFile make sure there is no file at path after checking it is safe to write to that file
func removeFile(path string) error {
err := checkCanWrite(path)
if err != nil {
return err
}
err = os.Remove(path)
if e, ok := err.(*os.PathError); ok && os.IsNotExist(e.Err) {
err = nil
}
return err
}
// Program represents loaded program for tracepoint analysis.
//
// It is generalization of loader.Program due to loader not allowing to
// construct programs incrementally.
type Program struct {
// list of loader.Programs in use
//
// We generally need to have several programs because a package can
// trace:import another package which is not otherwise imported by
// original program.
//
// Since go/loader does not support incrementally augmenting loaded
// program with more packages, we work-around it with having several
// progs.
progv []*loader.Program
// config for loading programs
loaderConf *loader.Config
}
// NewProgram constructs new empty Program ready to load packages according to specified build context
func NewProgram(ctxt *build.Context, cwd string) *Program {
// adjust build context to filter-out ztrace* files when discovering packages
//
// we don't load what should be generated by us for 2 reasons:
// - code generated could be wrong with older version of the
// tool - it should not prevent from regenerating.
// - generated code imports packages which might be not there
// yet in gopath (lab.nexedi.com/kirr/go123/tracing)
ctxtReadDir := ctxt.ReadDir
if ctxtReadDir == nil {
ctxtReadDir = ioutil.ReadDir
}
ctxtNoZTrace := *ctxt
ctxtNoZTrace.ReadDir = func(dir string) ([]os.FileInfo, error) {
fv, err := ctxtReadDir(dir)
okv := fv[:0]
for _, f := range fv {
if !strings.HasPrefix(f.Name(), "ztrace") {
okv = append(okv, f)
}
}
return okv, err
}
p := &Program{}
p.loaderConf = &loader.Config{
ParserMode: parser.ParseComments,
TypeCheckFuncBodies: func(path string) bool { return false },
Build: &ctxtNoZTrace,
Cwd: cwd,
}
return p
}
// Import imports a package and returns associated package info and program
// under which it was loaded.
func (p *Program) Import(pkgpath string) (prog *loader.Program, pkgi *loader.PackageInfo, err error) {
// let's see - maybe it is already there
for _, prog := range p.progv {
pkgi := prog.Package(pkgpath)
if pkgi != nil {
return prog, pkgi, nil
}
}
// not found - we have to load new program rooted at pkgpath
p.loaderConf.ImportPkgs = nil
p.loaderConf.Import(pkgpath)
prog, err = p.loaderConf.Load()
if err != nil {
return nil, nil, err
}
if !(len(prog.Created) == 0 && len(prog.Imported) == 1) {
panic("import")
}
p.progv = append(p.progv, prog)
pkgi = prog.InitialPackages()[0]
return prog, pkgi, nil
}
// ImportWithTests imports a package augmented with code from _test.go files +
// imports external test package (if present).
func (p *Program) ImportWithTests(pkgpath string) (prog *loader.Program, pkgi *loader.PackageInfo, xtestPkgi *loader.PackageInfo, err error) {
// NOTE always reimporting not to interfere with regular imports
p.loaderConf.ImportPkgs = nil
p.loaderConf.ImportWithTests(pkgpath)
prog, err = p.loaderConf.Load()
if err != nil {
return nil, nil, nil, err
}
if len(prog.Imported) != 1 {
panic("import with tests")
}
if len(prog.Created) > 0 {
xtestPkgi = prog.Created[0]
}
for _, pkgi = range prog.Imported {
}
return prog, pkgi, xtestPkgi, nil
}
// ---- `gotrace gen` ----
// tracegen generates code according to tracing directives in a package @ pkgpath.
//
// ctxt is build context for discovering packages
// cwd is "current" directory for resolving local imports (e.g. packages like "./some/package")
func tracegen(pkgpath string, ctxt *build.Context, cwd string) error {
P := NewProgram(ctxt, cwd)
lprog, pkgi, xtestPkgi, err := P.ImportWithTests(pkgpath)
if err != nil {
return err
}
// determine package directory
if len(pkgi.Files) == 0 {
return fmt.Errorf("package %s is empty", pkgi.Pkg.Path)
}
pkgdir := filepath.Dir(lprog.Fset.File(pkgi.Files[0].Pos()).Name())
// tracing info for this specified package
tpkg, err := packageTrace(lprog, pkgi)
if err != nil {
return err // XXX err ctx
}
// split everything related to tracing into plain and test (not xtest) packages
testTpkg := tpkg.SplitTests()
err1 := tracegen1(P, tpkg, pkgdir, "")
err2 := tracegen1(P, testTpkg, pkgdir, "_test")
// also handle xtest package
xtestTpkg := &Package{} // dummy package with empty .Eventv & .Importv
if xtestPkgi != nil {
xtestTpkg, err = packageTrace(lprog, xtestPkgi)
if err != nil {
return err // XXX err ctx
}
}
err3 := tracegen1(P, xtestTpkg, pkgdir, "_x_test")
return xerr.Merge(err1, err2, err3)
}
// tracegen1 generates code according to tracing directives for a (sub)package @pkgpath.
//
// subpackage is either original package, testing code, or external test package
func tracegen1(P *Program, tpkg *Package, pkgdir string, kind string) error {
var err error
// write ztrace.go with code generated for trace events and imports
ztrace_go := filepath.Join(pkgdir, "ztrace"+kind+".go")
if len(tpkg.Eventv) == 0 && len(tpkg.Importv) == 0 {
err = removeFile(ztrace_go)
if err != nil {
return err
}
} else {
// prologue
prologue := &Buffer{}
prologue.WriteString(magic)
prologue.emit("\npackage %v", tpkg.Pkgi.Pkg.Name())
prologue.emit("// code generated for tracepoints")
prologue.emit("\nimport (")
prologue.emit("\t%q", "lab.nexedi.com/kirr/neo/go/xcommon/tracing")
// pkgpaths of all packages needed for used types
needPkg := StrSet{}
// some packages are imported with explicit name
importedAs := map[string]string{} // pkgpath -> pkgname
text := &Buffer{}
// code for trace:event definitions
for _, event := range tpkg.Eventv {
needPkg.Add("unsafe") // used in tr
needPkg.Add(event.NeedPkgv()...)
err = traceEventCodeTmpl.Execute(text, event)
if err != nil {
panic(err)
}
}
// export hash symbol so that if importing package is out of
// sync - it will have it different and linking will fail.
if len(tpkg.Eventv) > 0 {
text.emit("\n// trace export signature")
//text.emit("---- 8< ----")
//fmt.Fprintf(text, "%s", traceExport(tpkg, kind))
//text.emit("---- 8< ----")
text.emit("func _trace_exporthash_%s() {}", traceExportHash(tpkg, kind))
}
// code for trace:import imports
for _, timport := range tpkg.Importv {
text.emit("\n// traceimport: %s", timport.ImportSpec())
impProg, impPkgi, err := P.Import(timport.PkgPath)
if err != nil {
return fmt.Errorf("%v: error trace-importing %s: %v", timport.Pos, timport.PkgPath, err)
}
// set name of the package if it was not explicitly specified
if timport.PkgName == "" {
timport.PkgName = impPkgi.Pkg.Name()
} else {
importedAs[timport.PkgPath] = timport.PkgName
}
impPkg, err := packageTrace(impProg, impPkgi)
if err != nil {
return err // XXX err ctx
}
if len(impPkg.Eventv) == 0 {
return fmt.Errorf("%v: package %v does not export anything trace-related", timport.Pos, timport.PkgPath)
}
// verify export hash so link fails if it gets out of sync with imported package
err = traceEventImportCheckTmpl.Execute(text, struct {
ImportSpec *traceImport
ExportHash string
}{
timport,
traceExportHash(impPkg, "" /*regular package*/)})
text.emit("")
// import individual events
for _, event := range impPkg.Eventv {
needPkg.Add(event.NeedPkgv()...)
importedEvent := traceImported{
traceEvent: event,
ImportSpec: timport,
ImporterPkg: tpkg.Pkgi.Pkg,
ImportedAs: importedAs,
}
err = traceEventImportTmpl.Execute(text, importedEvent)
if err != nil {
panic(err)
}
}
}
// finish prologue with needed imports
if !needPkg.Has("unsafe") {
// we need it anyway because go:linkname is not allowed without unsafe
prologue.emit("\t_ %q", "unsafe")
} else {
prologue.emit("\t%q", "unsafe")
needPkg.Delete("unsafe")
}
needPkg.Delete(tpkg.Pkgi.Pkg.Path()) // our pkg - no need to import
needPkgv := needPkg.Itemv()
if len(needPkgv) > 0 {
prologue.emit("")
}
for _, needpkg := range needPkgv {
pkgname := importedAs[needpkg]
if pkgname != "" {
pkgname += " "
}
prologue.emit("\t%s%q", pkgname, needpkg)
}
prologue.emit(")")
// write output to ztrace.go
fulltext := append(prologue.Bytes(), text.Bytes()...)
err = writeFile(ztrace_go, fulltext)
if err != nil {
return err
}
}
// write empty ztrace.s so go:linkname works, if there are trace imports
ztrace_s := filepath.Join(pkgdir, "ztrace"+kind+".s")
if len(tpkg.Importv) == 0 {
err = removeFile(ztrace_s)
} else {
text := &Buffer{}
text.WriteString(magic)
text.emit("// empty .s so `go build` does not use -complete for go:linkname to work")
err = writeFile(ztrace_s, text.Bytes())
}
if err != nil {
return err
}
return nil
}
// traceExport returns signatures of all tracing-related exports of a package
// in canonical order as would be seen from universe scope.
func traceExport(tpkg *Package, kind string) []byte {
pkgpath := tpkg.Pkgi.Pkg.Path()
pkgname := tpkg.Pkgi.Pkg.Name()
exported := &Buffer{}
exported.emit("%q %q", pkgpath, kind)
for _, event := range tpkg.Eventv {
importedEvent := traceImported{
traceEvent: event,
ImportSpec: &traceImport{PkgName: pkgname, PkgPath: pkgpath},
ImporterPkg: nil, // from nowhere
ImportedAs: nil, // no naming for imports
}
err := traceEventImportTmpl.Execute(exported, importedEvent)
if err != nil {
panic(err)
}
}
return exported.Bytes()
}
// traceExportHash computes signature of tracing-related exports of a package
// implementation note: it is sha1 of associated header + importing code as
// if it was executed from universe scope.
func traceExportHash(tpkg *Package, kind string) string {
return fmt.Sprintf("%x", sha1.Sum(traceExport(tpkg, kind)))
}
const genSummary = "generate code according to tracing annotations and imports"
func genUsage(w io.Writer) {
fmt.Fprintf(w,
`Usage: gotrace gen <package>
Generate code according to tracing annotations and imports
options:
-h --help this help text.
`)
}
func genMain(argv []string) {
flags := flag.FlagSet{Usage: func() { genUsage(os.Stderr) }}
flags.Init("", flag.ExitOnError)
flags.Parse(argv[1:])
argv = flags.Args()
if len(argv) < 1 {
flags.Usage()
prog.Exit(2)
}
pkgpath := argv[0]
cwd, err := os.Getwd()
if err != nil {
prog.Fatal(err)
}
err = tracegen(pkgpath, &build.Default, cwd)
if err != nil {
prog.Fatal(err)
}
}
// ---- `gotrace list` ----
// tracelist lists trace-events defined by a package @ pkgpath.
//
// ctxt and cwd are tunables for discovering packages. See tracegen for details.
//
// TODO support listing by pkgspec (e.g. "./...")
func tracelist(w io.Writer, pkgpath string, ctxt *build.Context, cwd string) error {
P := NewProgram(ctxt, cwd)
// NOTE only listing trace-events provided by main package, not tests or xtest
lprog, pkgi, err := P.Import(pkgpath)
if err != nil {
return err
}
tpkg, err := packageTrace(lprog, pkgi)
if err != nil {
return err // XXX err ctx
}
for _, event := range tpkg.Eventv {
_, err = fmt.Fprintf(w, "%s:%s\n", event.Pkgt.Pkgi.Pkg.Path(), event.Name)
if err != nil {
return err
}
}
return nil
}
const listSummary = "lists tracepoints defined by a package"
func listUsage(w io.Writer) {
fmt.Fprintf(w,
`Usage: gotrace list <package>
List tracepoints defined by a package
options:
-h --help this help text.
`)
}
func listMain(argv []string) {
flags := flag.FlagSet{Usage: func() { genUsage(os.Stderr) }}
flags.Init("", flag.ExitOnError)
flags.Parse(argv[1:])
argv = flags.Args()
if len(argv) < 1 {
flags.Usage()
prog.Exit(2)
}
pkgpath := argv[0]
cwd, err := os.Getwd()
if err != nil {
prog.Fatal(err)
}
err = tracelist(os.Stdout, pkgpath, &build.Default, cwd)
if err != nil {
prog.Fatal(err)
}
}
// ---- main driver ----
var commands = prog.CommandRegistry{
{"gen", genSummary, genUsage, genMain},
{"list", listSummary, listUsage, listMain},
}
var helpTopics = prog.HelpRegistry{
// XXX for now empty
}
var gotrace = prog.MainProg{
Name: "gotrace",
Summary: "Gotrace is a program to support and interact with go tracing subsystem",
Commands: commands,
HelpTopics: helpTopics,
}
func main() {
log.SetFlags(0)
log.SetPrefix("gotrace: ")
gotrace.Main()
}
// Copyright (C) 2017 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
package main
import (
"bytes"
"fmt"
"go/build"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"syscall"
"testing"
"lab.nexedi.com/kirr/go123/exc"
)
func xglob(t *testing.T, pattern string) []string {
t.Helper()
matchv, err := filepath.Glob(pattern)
if err != nil {
t.Fatal(err)
}
return matchv
}
type TreePrepareMode int
const (
TreePrepareGolden TreePrepareMode = iota // prepare golden tree - how `gotrace gen` result should look like
TreePrepareWork // prepare work tree - inital state for `gotrace gen` to run
)
// prepareTestTree copies files from src to dst recursively processing *.ok and *.rm depending on mode.
//
// dst should not initially exist
func prepareTestTree(src, dst string, mode TreePrepareMode) error {
err := os.MkdirAll(dst, 0777)
if err != nil {
return err
}
return filepath.Walk(src, func(srcpath string, info os.FileInfo, err error) error {
if srcpath == src /* skip root */ || err != nil {
return err
}
dstpath := dst + strings.TrimPrefix(srcpath, src)
if info.IsDir() {
err := os.Mkdir(dstpath, 0777)
return err
}
// NOTE since files are walked in lexical order <f>.ok or
// <f>.rm is always guaranteed to go after <f>.
var isOk, isRm bool
if strings.HasSuffix(srcpath, ".ok") {
isOk = true
dstpath = strings.TrimSuffix(dstpath, ".ok")
}
if strings.HasSuffix(srcpath, ".rm") {
isRm = true
dstpath = strings.TrimSuffix(dstpath, ".rm")
}
data, err := ioutil.ReadFile(srcpath)
if err != nil {
return err
}
switch mode {
case TreePrepareGolden:
// ok files are written as is
// no removed files
if isRm {
return nil
}
case TreePrepareWork:
// no ok files initially
if isOk {
return nil
}
// files to remove - prepopulate with magic
if isRm {
data = []byte(magic)
}
}
err = ioutil.WriteFile(dstpath, data, info.Mode())
return err
})
}
func xprepareTree(src, dst string, mode TreePrepareMode) {
err := prepareTestTree(src, dst, mode)
exc.Raiseif(err)
}
// diffR compares two directories recursively
func diffR(patha, pathb string) (diff string, err error) {
cmd := exec.Command("diff", "-urN", patha, pathb)
out, err := cmd.Output()
if e, ok := err.(*exec.ExitError); ok {
if e.Sys().(syscall.WaitStatus).ExitStatus() == 1 {
err = nil // diff signals with 1 just a difference - problem exit code is 2
} else {
err = fmt.Errorf("diff %s %s:\n%s", patha, pathb, e.Stderr)
}
}
return string(out), err
}
func TestGoTrace(t *testing.T) {
tmp, err := ioutil.TempDir("", "t-gotrace")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmp)
good := tmp + "/good"
work := tmp + "/work"
xprepareTree("testdata", good, TreePrepareGolden)
xprepareTree("testdata", work, TreePrepareWork)
// test build context with GOPATH set to work tree
var tBuildCtx = &build.Context{
GOARCH: "amd64",
GOOS: "linux",
GOROOT: runtime.GOROOT(),
GOPATH: work,
CgoEnabled: true,
Compiler: runtime.Compiler,
}
// XXX autodetect (go list ?)
testv := []string{"a/pkg1", "b/pkg2", "c/pkg3", "d/pkg4"}
for _, tpkg := range testv {
// verify `gotrace gen`
err = tracegen(tpkg, tBuildCtx, "" /* = local imorts disabled */)
if err != nil {
t.Errorf("%v: %v", tpkg, err)
}
diff, err := diffR(good+"/src/"+tpkg, work+"/src/"+tpkg)
if err != nil {
t.Fatalf("%v: %v", tpkg, err)
}
if diff != "" {
t.Errorf("%v: gold & work differ:\n%s", tpkg, diff)
}
// verify `gotrace list`
var tlistBuf bytes.Buffer
err = tracelist(&tlistBuf, tpkg, tBuildCtx, "" /* = local imports disabled */)
if err != nil {
t.Fatalf("%v: %v", tpkg, err)
}
tlistOk, err := ioutil.ReadFile(work + "/src/" + tpkg + "/tracelist.txt")
if err != nil {
t.Fatalf("%v: %v", tpkg, err)
}
tlist := tlistBuf.Bytes()
if !bytes.Equal(tlist, tlistOk) {
t.Errorf("%v: tracelist differ:\nhave:\n%s\nwant:\n%s", tpkg, tlist, tlistOk)
}
}
}
package pkg1
import (
"net/url"
// extra import which is used in package but should not be used in tracing code
"fmt"
)
// probe receives no args
//trace:event traceNewTPre()
// probe receives type this package defines
//trace:event traceNewT(t *T)
type T struct {}
func NewT() *T {
traceNewTPre()
t := &T{}
traceNewT(t)
return t
}
// probe receives type from another package
//trace:event traceURLParsed(u *url.URL)
func ParseURL(ustr string) (*url.URL, error) {
u, err := url.Parse(ustr)
if err != nil {
return nil, fmt.Errorf("oh my bad: %v", err)
}
traceURLParsed(u)
return u, nil
}
// probe receives builtin type
//trace:event traceDoSomething(topic string)
func DoSomething(topic string) {
traceDoSomething(topic)
}
// XXX do we need vvv ?
// package-local non-exported tracepoint
//type t struct {}
////trace:event tracedoSomethingLocal(arg *t)
package pkg1
/*
#include <stdio.h>
void hello() {
printf("Hello World (from C)\n");
}
*/
import "C"
// FIXME vvv does not currently work because go/loader gives us already
// preprocessed result without original comments.
//
// trace event defined in a cgo file
//trace:event traceHello()
func Hello() {
//traceHello()
C.hello()
}
a/pkg1:traceDoSomething
a/pkg1:traceNewT
a/pkg1:traceNewTPre
a/pkg1:traceURLParsed
// Code generated by lab.nexedi.com/kirr/go123/tracing/cmd/gotrace; DO NOT EDIT.
Bad bad bad - I'm invalid go file.
// Code generated by lab.nexedi.com/kirr/go123/tracing/cmd/gotrace; DO NOT EDIT.
package pkg1
// code generated for tracepoints
import (
"lab.nexedi.com/kirr/neo/go/xcommon/tracing"
"unsafe"
"net/url"
)
// traceevent: traceDoSomething(topic string)
type _t_traceDoSomething struct {
tracing.Probe
probefunc func(topic string)
}
var _traceDoSomething *_t_traceDoSomething
func traceDoSomething(topic string) {
if _traceDoSomething != nil {
_traceDoSomething_run(topic)
}
}
func _traceDoSomething_run(topic string) {
for p := _traceDoSomething; p != nil; p = (*_t_traceDoSomething)(unsafe.Pointer(p.Next())) {
p.probefunc(topic)
}
}
func traceDoSomething_Attach(pg *tracing.ProbeGroup, probe func(topic string)) *tracing.Probe {
p := _t_traceDoSomething{probefunc: probe}
tracing.AttachProbe(pg, (**tracing.Probe)(unsafe.Pointer(&_traceDoSomething)), &p.Probe)
return &p.Probe
}
// traceevent: traceNewT(t *T)
type _t_traceNewT struct {
tracing.Probe
probefunc func(t *T)
}
var _traceNewT *_t_traceNewT
func traceNewT(t *T) {
if _traceNewT != nil {
_traceNewT_run(t)
}
}
func _traceNewT_run(t *T) {
for p := _traceNewT; p != nil; p = (*_t_traceNewT)(unsafe.Pointer(p.Next())) {
p.probefunc(t)
}
}
func traceNewT_Attach(pg *tracing.ProbeGroup, probe func(t *T)) *tracing.Probe {
p := _t_traceNewT{probefunc: probe}
tracing.AttachProbe(pg, (**tracing.Probe)(unsafe.Pointer(&_traceNewT)), &p.Probe)
return &p.Probe
}
// traceevent: traceNewTPre()
type _t_traceNewTPre struct {
tracing.Probe
probefunc func()
}
var _traceNewTPre *_t_traceNewTPre
func traceNewTPre() {
if _traceNewTPre != nil {
_traceNewTPre_run()
}
}
func _traceNewTPre_run() {
for p := _traceNewTPre; p != nil; p = (*_t_traceNewTPre)(unsafe.Pointer(p.Next())) {
p.probefunc()
}
}
func traceNewTPre_Attach(pg *tracing.ProbeGroup, probe func()) *tracing.Probe {
p := _t_traceNewTPre{probefunc: probe}
tracing.AttachProbe(pg, (**tracing.Probe)(unsafe.Pointer(&_traceNewTPre)), &p.Probe)
return &p.Probe
}
// traceevent: traceURLParsed(u *url.URL)
type _t_traceURLParsed struct {
tracing.Probe
probefunc func(u *url.URL)
}
var _traceURLParsed *_t_traceURLParsed
func traceURLParsed(u *url.URL) {
if _traceURLParsed != nil {
_traceURLParsed_run(u)
}
}
func _traceURLParsed_run(u *url.URL) {
for p := _traceURLParsed; p != nil; p = (*_t_traceURLParsed)(unsafe.Pointer(p.Next())) {
p.probefunc(u)
}
}
func traceURLParsed_Attach(pg *tracing.ProbeGroup, probe func(u *url.URL)) *tracing.Probe {
p := _t_traceURLParsed{probefunc: probe}
tracing.AttachProbe(pg, (**tracing.Probe)(unsafe.Pointer(&_traceURLParsed)), &p.Probe)
return &p.Probe
}
// trace export signature
func _trace_exporthash_965fa599dc3a61119faba1eacf8493973c5d87ad() {}
package pkg2
// trace-import another package
// NOTE "a/pkg1" is not regularly imported
//trace:import "a/pkg1"
// additional tracepoint which pkg2 defines
//trace:event traceDoSomething(i, j int, q string)
func DoSomething(i, j int, q string) {
traceDoSomething(i, j, q)
}
// Code generated by lab.nexedi.com/kirr/go123/tracing/cmd/gotrace; DO NOT EDIT.
package pkg2
// code generated for tracepoints
import (
"lab.nexedi.com/kirr/neo/go/xcommon/tracing"
"unsafe"
"a/pkg1"
"net/url"
)
// traceevent: traceDoSomething(i, j int, q string)
type _t_traceDoSomething struct {
tracing.Probe
probefunc func(i, j int, q string)
}
var _traceDoSomething *_t_traceDoSomething
func traceDoSomething(i, j int, q string) {
if _traceDoSomething != nil {
_traceDoSomething_run(i, j, q)
}
}
func _traceDoSomething_run(i, j int, q string) {
for p := _traceDoSomething; p != nil; p = (*_t_traceDoSomething)(unsafe.Pointer(p.Next())) {
p.probefunc(i, j, q)
}
}
func traceDoSomething_Attach(pg *tracing.ProbeGroup, probe func(i, j int, q string)) *tracing.Probe {
p := _t_traceDoSomething{probefunc: probe}
tracing.AttachProbe(pg, (**tracing.Probe)(unsafe.Pointer(&_traceDoSomething)), &p.Probe)
return &p.Probe
}
// trace export signature
func _trace_exporthash_80ddfc2f6c72bdf357dedbb2f0bbec85e93106fc() {}
// traceimport: "a/pkg1"
// rerun "gotrace gen" if you see link failure ↓↓↓
//go:linkname pkg1_trace_exporthash a/pkg1._trace_exporthash_965fa599dc3a61119faba1eacf8493973c5d87ad
func pkg1_trace_exporthash()
func init() { pkg1_trace_exporthash() }
//go:linkname pkg1_traceDoSomething_Attach a/pkg1.traceDoSomething_Attach
func pkg1_traceDoSomething_Attach(*tracing.ProbeGroup, func(topic string)) *tracing.Probe
//go:linkname pkg1_traceNewT_Attach a/pkg1.traceNewT_Attach
func pkg1_traceNewT_Attach(*tracing.ProbeGroup, func(t *pkg1.T)) *tracing.Probe
//go:linkname pkg1_traceNewTPre_Attach a/pkg1.traceNewTPre_Attach
func pkg1_traceNewTPre_Attach(*tracing.ProbeGroup, func()) *tracing.Probe
//go:linkname pkg1_traceURLParsed_Attach a/pkg1.traceURLParsed_Attach
func pkg1_traceURLParsed_Attach(*tracing.ProbeGroup, func(u *url.URL)) *tracing.Probe
// Code generated by lab.nexedi.com/kirr/go123/tracing/cmd/gotrace; DO NOT EDIT.
// empty .s so `go build` does not use -complete for go:linkname to work
package pkg3_test
import "testing"
// trace import that should be added only to external tests
//trace:import "b/pkg2"
func TestZzzExternal(t *testing.T) {
t.Fatal("...")
}
package pkg3
import "testing"
// trace import that should be added only to tests, and under specified package name
//trace:import aaa1 "a/pkg1"
func TestZzz(t *testing.T) {
if zzz() != 2 {
t.Fatal("zzz wrong")
}
}
// Code generated by lab.nexedi.com/kirr/go123/tracing/cmd/gotrace; DO NOT EDIT.
package pkg3
// code generated for tracepoints
import (
"lab.nexedi.com/kirr/neo/go/xcommon/tracing"
_ "unsafe"
aaa1 "a/pkg1"
"net/url"
)
// traceimport: aaa1 "a/pkg1"
// rerun "gotrace gen" if you see link failure ↓↓↓
//go:linkname aaa1_trace_exporthash a/pkg1._trace_exporthash_965fa599dc3a61119faba1eacf8493973c5d87ad
func aaa1_trace_exporthash()
func init() { aaa1_trace_exporthash() }
//go:linkname aaa1_traceDoSomething_Attach a/pkg1.traceDoSomething_Attach
func aaa1_traceDoSomething_Attach(*tracing.ProbeGroup, func(topic string)) *tracing.Probe
//go:linkname aaa1_traceNewT_Attach a/pkg1.traceNewT_Attach
func aaa1_traceNewT_Attach(*tracing.ProbeGroup, func(t *aaa1.T)) *tracing.Probe
//go:linkname aaa1_traceNewTPre_Attach a/pkg1.traceNewTPre_Attach
func aaa1_traceNewTPre_Attach(*tracing.ProbeGroup, func()) *tracing.Probe
//go:linkname aaa1_traceURLParsed_Attach a/pkg1.traceURLParsed_Attach
func aaa1_traceURLParsed_Attach(*tracing.ProbeGroup, func(u *url.URL)) *tracing.Probe
// Code generated by lab.nexedi.com/kirr/go123/tracing/cmd/gotrace; DO NOT EDIT.
// empty .s so `go build` does not use -complete for go:linkname to work
// Code generated by lab.nexedi.com/kirr/go123/tracing/cmd/gotrace; DO NOT EDIT.
package pkg3_test
// code generated for tracepoints
import (
"lab.nexedi.com/kirr/neo/go/xcommon/tracing"
_ "unsafe"
)
// traceimport: "b/pkg2"
// rerun "gotrace gen" if you see link failure ↓↓↓
//go:linkname pkg2_trace_exporthash b/pkg2._trace_exporthash_80ddfc2f6c72bdf357dedbb2f0bbec85e93106fc
func pkg2_trace_exporthash()
func init() { pkg2_trace_exporthash() }
//go:linkname pkg2_traceDoSomething_Attach b/pkg2.traceDoSomething_Attach
func pkg2_traceDoSomething_Attach(*tracing.ProbeGroup, func(i, j int, q string)) *tracing.Probe
// Code generated by lab.nexedi.com/kirr/go123/tracing/cmd/gotrace; DO NOT EDIT.
// empty .s so `go build` does not use -complete for go:linkname to work
package pkg4
// this package does not use tracepoints at all
// Copyright (C) 2017 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
package main
// misc utilities
import (
"bytes"
"fmt"
"sort"
)
// Buffer is bytes.Buffer + syntatic sugar
type Buffer struct {
bytes.Buffer
}
func (b *Buffer) emit(format string, argv ...interface{}) {
fmt.Fprintf(b, format+"\n", argv...)
}
// StrSet is set<string>
type StrSet map[string]struct{}
func (s StrSet) Add(itemv ...string) {
for _, item := range itemv {
s[item] = struct{}{}
}
}
func (s StrSet) Delete(item string) {
delete(s, item)
}
func (s StrSet) Has(item string) bool {
_, has := s[item]
return has
}
// Itemv returns ordered slice of set items
func (s StrSet) Itemv() []string {
itemv := make([]string, 0, len(s))
for item := range s {
itemv = append(itemv, item)
}
sort.Strings(itemv)
return itemv
}
// Copyright (C) 2017 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
// +build race
package race
import "unsafe"
/*
// symbols are e.g. in go/src/runtime/race/race_linux_amd64.syso
#cgo LDFLAGS: -Wl,--unresolved-symbols=ignore-in-object-files
// __tsan::ThreadIgnoreBegin(__tsan::ThreadState*, unsigned long)
// __tsan::ThreadIgnoreEnd(__tsan::ThreadState*, unsigned long)
extern void _ZN6__tsan17ThreadIgnoreBeginEPNS_11ThreadStateEm(void *, unsigned long);
extern void _ZN6__tsan15ThreadIgnoreEndEPNS_11ThreadStateEm(void *, unsigned long);
*/
import "C"
// Ways to tell race-detector to ignore "read/write" events from current thread.
// NOTE runtime.RaceDisable disables only "sync" part, not "read/write".
func IgnoreBegin(racectx uintptr) {
C._ZN6__tsan17ThreadIgnoreBeginEPNS_11ThreadStateEm(unsafe.Pointer(racectx), 0)
}
func IgnoreEnd(racectx uintptr) {
C._ZN6__tsan15ThreadIgnoreEndEPNS_11ThreadStateEm(unsafe.Pointer(racectx), 0)
}
// Copyright (C) 2017 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
// +build !race
package race
// race ignore begin/end stubs
func IgnoreBegin(racectx uintptr) {}
func IgnoreEnd(racectx uintptr) {}
#!/bin/bash
# g_typedef -- generate type definition of runtime.g
goexec= # set by main driver to current go compiler
gover= # go<maj>.<min> version (without .patch)
gomaj= # go<maj>
gomin= # <min>
govern= # e.g. 109 for go1.9
# goset <goexec> - set <goexec> as current go
goset() {
goexec=$1
# go1.1 go1.2 go1.3 go1.4 go1.5 go1.6 go1.7 go1.8 go1.9 go1.10 -> go1.10
gover=`$goexec list -f '{{ range context.ReleaseTags }} {{ .}}{{end}}' runtime |awk '{print $NF}'`
IFS=. read gomaj gomin < <(echo "$gover")
govern=$((${gomaj#go} * 100 + $gomin))
}
# typedef <type> - print type definition
typedef() {
$goexec doc -c -u $1 |sed -n -e '/^type /,/^}/p'
}
# typedef_g - print <g> & friends definitions
typedef_g() {
typedef runtime.g
typedef runtime.stack
typedef runtime._panic
typedef runtime._defer
typedef runtime.gobuf
typedef runtime.funcval
#typedef runtime.sudog
#typedef runtime.hchan
typedef runtime.timer
#typedef runtime.guintptr
#typedef runtime.m
if (( $govern < 109 )); then
typedef runtime.stkbar
fi
}
# typedef_g_fixed - print adjusted <g> & friends definitions
typedef_g_fixed() {
typedef_g $1 | \
sed -e 's/\<sys.Uintreg\>/uintreg/'
echo "type guintptr uintptr // XXX stub"
echo "type puintptr uintptr // XXX stub"
echo "type uintreg uint // FIXME wrong on amd64p32"
echo "type m struct {} // FIXME stub"
echo "type sudog struct {} // FIXME stub"
}
# gen_zruntime - generate zruntime_g_<gover>.go
gen_zruntime() {
out="zruntime_g_$gover.go"
echo >$out "// Code generated by g_typedef; DO NOT EDIT."
echo >>$out
echo >>$out "// +build $gover,!$gomaj.$((gomin+1))"
echo >>$out
echo >>$out "package xruntime"
echo >>$out
echo >>$out 'import "unsafe"'
echo >>$out
typedef_g_fixed $go >>$out
}
# main driver
gov="go18 go"
for g in $gov; do
goset $g
gen_zruntime
done
// Copyright (C) 2017 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
// +build race
package xruntime
import "lab.nexedi.com/kirr/neo/go/xcommon/tracing/internal/race"
// RaceIgnoreBegin instructs race-detector to ignore memory read/write events from current goroutine.
//
// The events will be back to handled after call to RaceIgnoreEnd.
//
// NOTE runtime.RaceDisable disables "sync" events, not memory read/write.
func RaceIgnoreBegin() {
race.IgnoreBegin(getg().racectx)
}
// RaceIgnoreEnd instructs race-detector to stop ignoring memory read/write events from current goroutine.
//
// NOTE runtime RaceEnable enables "sync" events, not memory read/write.
func RaceIgnoreEnd() {
race.IgnoreEnd(getg().racectx)
}
// Copyright (C) 2017 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
// +build !race
package xruntime
// empty stubs for Race*()
func RaceIgnoreBegin() {}
func RaceIgnoreEnd() {}
// Copyright (C) 2016-2017 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
package xruntime
// stop-the-world that should probably be in public xruntime, but I'm (yet)
// hesitating to expose the API to public.
import _ "unsafe"
//go:linkname runtime_stopTheWorld runtime.stopTheWorld
//go:linkname runtime_startTheWorld runtime.startTheWorld
func runtime_stopTheWorld(reason string)
func runtime_startTheWorld()
// StopTheWorld returns with the world stopped.
//
// Current goroutine remains the only one who is running, with others
// goroutines stopped at safe GC points.
// It requires careful programming as many things that normally work lead to
// fatal errors when the world is stopped - for example using timers would be
// invalid, but adjusting plain values in memory is ok.
func StopTheWorld(reason string) {
runtime_stopTheWorld(reason)
}
// StartTheWorld restarts the world after it was stopped by StopTheWorld.
func StartTheWorld() {
runtime_startTheWorld()
}
// empty .s so `go build` does not use -complete for go:linkname to work
// Copyright (C) 2017 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
package xruntime
//go:generate ./g_typedef
// getg returns pointer to current goroutine descriptor.
func getg() *g
#include "textflag.h"
// +build 386
// func getg() *g
TEXT ·getg(SB),NOSPLIT,$0-8
MOVL (TLS), AX
MOVL AX, ret+0(FP)
RET
#include "textflag.h"
// +build amd64 amd64p
// func getg() *g
TEXT ·getg(SB),NOSPLIT,$0-8
MOVQ (TLS), R14
MOVQ R14, ret+0(FP)
RET
package xruntime
import (
"runtime"
"sync/atomic"
"testing"
"time"
)
func TestStartStopTheWorld(t *testing.T) {
var x, stop int32
ready := make(chan int)
go func() {
// make sure the thread running this goroutine is different from thread for main g.
// this way we can be sure there are 2 OS threads in action and communicating via busywait should work.
runtime.LockOSThread()
ready <- 0
for atomic.LoadInt32(&stop) == 0 {
atomic.AddInt32(&x, 1)
// XXX as of go19 tight loops are not preemptible (golang.org/issues/10958)
// -> explicitly make sure we do not miss STW request.
runtime.Gosched()
}
}()
// wait for spawned goroutine to jump into its own thread
<-ready
// verify g and g2 are indeed running in parallel
xprev := atomic.LoadInt32(&x)
xnext := xprev
:= 0
tstart := time.Now()
for < 100 && time.Now().Sub(tstart) < time.Second {
xnext = atomic.LoadInt32(&x)
if xnext != xprev {
+= 1
xprev = xnext
}
}
if == 0 {
t.Fatal("g and g2 are not running in parallel")
}
// now stop the world and for 1s make sure g2 is not running in parallel with us
StopTheWorld("just for my reason")
xprev = atomic.LoadInt32(&x)
xnext = xprev
= 0
tstart = time.Now()
for time.Now().Sub(tstart) < time.Second {
for i := 0; i < 100 ; i++ {
xnext = atomic.LoadInt32(&x)
if xnext != xprev {
+= 1
xprev = xnext
}
}
}
StartTheWorld()
if != 0 {
t.Fatalf("g2 modified x at least %d times while the world was stopped", )
}
atomic.StoreInt32(&stop, 1)
}
// Code generated by g_typedef; DO NOT EDIT.
// +build go1.8,!go1.9
package xruntime
import "unsafe"
type g struct {
// Stack parameters.
// stack describes the actual stack memory: [stack.lo, stack.hi).
// stackguard0 is the stack pointer compared in the Go stack growth prologue.
// It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
// stackguard1 is the stack pointer compared in the C stack growth prologue.
// It is stack.lo+StackGuard on g0 and gsignal stacks.
// It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
stack stack // offset known to runtime/cgo
stackguard0 uintptr // offset known to liblink
stackguard1 uintptr // offset known to liblink
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
m *m // current m; offset known to arm liblink
stackAlloc uintptr // stack allocation is [stack.lo,stack.lo+stackAlloc)
sched gobuf
syscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gc
syscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gc
stkbar []stkbar // stack barriers, from low to high (see top of mstkbar.go)
stkbarPos uintptr // index of lowest stack barrier not hit
stktopsp uintptr // expected sp at top of stack, to check in traceback
param unsafe.Pointer // passed parameter on wakeup
atomicstatus uint32
stackLock uint32 // sigprof/scang lock; TODO: fold in to atomicstatus
goid int64
waitsince int64 // approx time when the g become blocked
waitreason string // if status==Gwaiting
schedlink guintptr
preempt bool // preemption signal, duplicates stackguard0 = stackpreempt
paniconfault bool // panic (instead of crash) on unexpected fault address
preemptscan bool // preempted g does scan for gc
gcscandone bool // g has scanned stack; protected by _Gscan bit in status
gcscanvalid bool // false at start of gc cycle, true if G has not run since last scan; transition from true to false by calling queueRescan and false to true by calling dequeueRescan
throwsplit bool // must not split stack
raceignore int8 // ignore race detection events
sysblocktraced bool // StartTrace has emitted EvGoInSyscall about this goroutine
sysexitticks int64 // cputicks when syscall has returned (for tracing)
traceseq uint64 // trace event sequencer
tracelastp puintptr // last P emitted an event for this goroutine
lockedm *m
sig uint32
writebuf []byte
sigcode0 uintptr
sigcode1 uintptr
sigpc uintptr
gopc uintptr // pc of go statement that created this goroutine
startpc uintptr // pc of goroutine function
racectx uintptr
waiting *sudog // sudog structures this g is waiting on (that have a valid elem ptr); in lock order
cgoCtxt []uintptr // cgo traceback context
// gcRescan is this G's index in work.rescan.list. If this is
// -1, this G is not on the rescan list.
//
// If gcphase != _GCoff and this G is visible to the garbage
// collector, writes to this are protected by work.rescan.lock.
gcRescan int32
// gcAssistBytes is this G's GC assist credit in terms of
// bytes allocated. If this is positive, then the G has credit
// to allocate gcAssistBytes bytes without assisting. If this
// is negative, then the G must correct this by performing
// scan work. We track this in bytes to make it fast to update
// and check for debt in the malloc hot path. The assist ratio
// determines how this corresponds to scan work debt.
gcAssistBytes int64
}
type stack struct {
lo uintptr
hi uintptr
}
type _panic struct {
argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
arg interface{} // argument to panic
link *_panic // link to earlier panic
recovered bool // whether this panic is over
aborted bool // the panic was aborted
}
type _defer struct {
siz int32
started bool
sp uintptr // sp at time of defer
pc uintptr
fn *funcval
_panic *_panic // panic that is running defer
link *_defer
}
type gobuf struct {
// The offsets of sp, pc, and g are known to (hard-coded in) libmach.
//
// ctxt is unusual with respect to GC: it may be a
// heap-allocated funcval so write require a write barrier,
// but gobuf needs to be cleared from assembly. We take
// advantage of the fact that the only path that uses a
// non-nil ctxt is morestack. As a result, gogo is the only
// place where it may not already be nil, so gogo uses an
// explicit write barrier. Everywhere else that resets the
// gobuf asserts that ctxt is already nil.
sp uintptr
pc uintptr
g guintptr
ctxt unsafe.Pointer // this has to be a pointer so that gc scans it
ret uintreg
lr uintptr
bp uintptr // for GOEXPERIMENT=framepointer
}
type funcval struct {
fn uintptr
}
type timer struct {
i int // heap index
// Timer wakes up at when, and then at when+period, ... (period > 0 only)
// each time calling f(arg, now) in the timer goroutine, so f must be
// a well-behaved function and not block.
when int64
period int64
f func(interface{}, uintptr)
arg interface{}
seq uintptr
}
type stkbar struct {
savedLRPtr uintptr // location overwritten by stack barrier PC
savedLRVal uintptr // value overwritten at savedLRPtr
}
type guintptr uintptr // XXX stub
type puintptr uintptr // XXX stub
type uintreg uint // FIXME wrong on amd64p32
type m struct {} // FIXME stub
type sudog struct {} // FIXME stub
// Code generated by g_typedef; DO NOT EDIT.
// +build go1.9,!go1.10
package xruntime
import "unsafe"
type g struct {
// Stack parameters.
// stack describes the actual stack memory: [stack.lo, stack.hi).
// stackguard0 is the stack pointer compared in the Go stack growth prologue.
// It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
// stackguard1 is the stack pointer compared in the C stack growth prologue.
// It is stack.lo+StackGuard on g0 and gsignal stacks.
// It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
stack stack // offset known to runtime/cgo
stackguard0 uintptr // offset known to liblink
stackguard1 uintptr // offset known to liblink
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
m *m // current m; offset known to arm liblink
sched gobuf
syscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gc
syscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gc
stktopsp uintptr // expected sp at top of stack, to check in traceback
param unsafe.Pointer // passed parameter on wakeup
atomicstatus uint32
stackLock uint32 // sigprof/scang lock; TODO: fold in to atomicstatus
goid int64
waitsince int64 // approx time when the g become blocked
waitreason string // if status==Gwaiting
schedlink guintptr
preempt bool // preemption signal, duplicates stackguard0 = stackpreempt
paniconfault bool // panic (instead of crash) on unexpected fault address
preemptscan bool // preempted g does scan for gc
gcscandone bool // g has scanned stack; protected by _Gscan bit in status
gcscanvalid bool // false at start of gc cycle, true if G has not run since last scan; TODO: remove?
throwsplit bool // must not split stack
raceignore int8 // ignore race detection events
sysblocktraced bool // StartTrace has emitted EvGoInSyscall about this goroutine
sysexitticks int64 // cputicks when syscall has returned (for tracing)
traceseq uint64 // trace event sequencer
tracelastp puintptr // last P emitted an event for this goroutine
lockedm *m
sig uint32
writebuf []byte
sigcode0 uintptr
sigcode1 uintptr
sigpc uintptr
gopc uintptr // pc of go statement that created this goroutine
startpc uintptr // pc of goroutine function
racectx uintptr
waiting *sudog // sudog structures this g is waiting on (that have a valid elem ptr); in lock order
cgoCtxt []uintptr // cgo traceback context
labels unsafe.Pointer // profiler labels
timer *timer // cached timer for time.Sleep
// gcAssistBytes is this G's GC assist credit in terms of
// bytes allocated. If this is positive, then the G has credit
// to allocate gcAssistBytes bytes without assisting. If this
// is negative, then the G must correct this by performing
// scan work. We track this in bytes to make it fast to update
// and check for debt in the malloc hot path. The assist ratio
// determines how this corresponds to scan work debt.
gcAssistBytes int64
}
type stack struct {
lo uintptr
hi uintptr
}
type _panic struct {
argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
arg interface{} // argument to panic
link *_panic // link to earlier panic
recovered bool // whether this panic is over
aborted bool // the panic was aborted
}
type _defer struct {
siz int32
started bool
sp uintptr // sp at time of defer
pc uintptr
fn *funcval
_panic *_panic // panic that is running defer
link *_defer
}
type gobuf struct {
// The offsets of sp, pc, and g are known to (hard-coded in) libmach.
//
// ctxt is unusual with respect to GC: it may be a
// heap-allocated funcval so write require a write barrier,
// but gobuf needs to be cleared from assembly. We take
// advantage of the fact that the only path that uses a
// non-nil ctxt is morestack. As a result, gogo is the only
// place where it may not already be nil, so gogo uses an
// explicit write barrier. Everywhere else that resets the
// gobuf asserts that ctxt is already nil.
sp uintptr
pc uintptr
g guintptr
ctxt unsafe.Pointer // this has to be a pointer so that gc scans it
ret uintreg
lr uintptr
bp uintptr // for GOEXPERIMENT=framepointer
}
type funcval struct {
fn uintptr
}
type timer struct {
i int // heap index
// Timer wakes up at when, and then at when+period, ... (period > 0 only)
// each time calling f(arg, now) in the timer goroutine, so f must be
// a well-behaved function and not block.
when int64
period int64
f func(interface{}, uintptr)
arg interface{}
seq uintptr
}
type guintptr uintptr // XXX stub
type puintptr uintptr // XXX stub
type uintreg uint // FIXME wrong on amd64p32
type m struct {} // FIXME stub
type sudog struct {} // FIXME stub
// Copyright (C) 2017 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
/*
Package tracing provides usage and runtime support for Go tracing facilities.
Trace events
A Go package can define several events of interest to trace via special
comments. With such definition a tracing event becomes associated with trace
function that is used to signal when the event happens. For example:
package hello
//trace:event traceHelloPre(who string)
//trace:event traceHello(who string)
func SayHello(who string) {
traceHelloPre(who)
fmt.Println("Hello, %s", who)
traceHello(who)
}
By default trace function does nothing and has very small overhead(*).
Probes
However it is possible to attach probing functions to events. A probe, once
attached, is called whenever event is signalled in the context which triggered
the event and pauses original code execution until the probe is finished. It is
possible to attach several probing functions to the same event and dynamically
detach/(re-)attach them at runtime. Attaching/detaching probes must be done
under tracing.Lock. For example:
type saidHelloT struct {
who string
when time.Time
}
saidHello := make(chan saidHelloT)
tracing.Lock()
p := traceHello_Attach(nil, func(who string) {
saidHello <- saidHelloT{who, time.Now()}
})
tracing.Unlock()
go func() {
for hello := range saidHello {
fmt.Printf("Said hello to %v @ %v\n", hello.who, hello.when)
}
}()
SayHello("JP")
SayHello("Kirr")
SayHello("Varya")
tracing.Lock()
p.Detach()
tracing.Unlock()
close(saidHello)
For convenience it is possible to keep group of attached probes and detach them
all at once using ProbeGroup:
pg := &tracing.ProbeGroup{}
tracing.Lock()
traceHelloPre_Attach(pg, func(who string) { ... })
traceHello_Attach(pg, func(who string) { ... })
tracing.Unlock()
// some activity
// when probes needs to be detached (no explicit tracing.Lock needed):
pg.Done()
Probes is general mechanism which allows various kinds of trace events usage.
Three ways particularly are well-understood and handy:
- recording events stream
- profiling
- synchronous tracing
Recording events stream
To get better understanding of what happens when it is possible to record
events into a stream and later either visualize or postprocess them.
This is similar to how Go execution tracer works:
https://golang.org/s/go15trace
https://golang.org/pkg/runtime/trace
https://golang.org/cmd/trace
though there it records only predefined set of events related to Go runtime.
TODO tracing should provide infrastructure to write events out in format
understood by chromium trace-viewer: https://github.com/catapult-project/catapult/tree/master/tracing
NOTE there is also talk/work to implement user events for runtime/trace: https://golang.org/issues/16619.
Profiling
A profile is aggregate summary of collection of stack traces showing the call sequences that led
to instances of a particular event. One could create runtime/pprof.Profile and
use Profile.Add in a probe attached to particular trace event. The profile can
be later analyzed and visualised with Profile.WriteTo and `go tool pprof`.
Please refer to runtime/pprof package documentation for details.
XXX Profile.Add needs unique value for each invocation - how do we do? Provide NaN every time?
XXX should tracing provide more tight integration with runtime/pprof.Profile?
Synchronous tracing
For testing purposes it is sometimes practical to leverage the property that
probes pause original code execution until the probe run is finished. That
means while the probe is running original goroutine
- is paused at well-defined point (where trace function is called), thus
- it cannot mutate any state it is programmed to mutate.
Using this properties it is possible to attach testing probes and verify that
a set of goroutines in tested code in question
- produce events in correct order, and
- at every event associated internal state is correct.
TODO example.
Cross package tracing
Trace events are not part of exported package API with rationale that package's
regular API and internal trace events usually have different stability
commitments. However with tracing-specific importing mechanism it is possible
to get access to trace events another package provides:
package another
//trace:import "hello"
This will make _Attach functions for all tracing events from package hello be
available as regular functions prefixed with imported package name:
tracing.Lock()
hello_traceHello_Attach(nil, func(who string) {
fmt.Printf("SayHello in package hello: %s", who)
tracing.Unlock()
...
Gotrace
The way //trace:event and //trace:import works is via additional code being
generated for them. Whenever a package uses any //trace: directive,
it has to organize to run `gotrace gen` on its sources for them to work,
usually with the help of //go:generate. For example:
package hello
//go:generate gotrace gen .
//trace:event ...
Besides `gotrace gen` gotrace has other subcommands also related to tracing,
for example `gotrace list` lists trace events a package provides.
Please see TODO link for gotrace documentation.
--------
(*) conditionally checking whether a pointer != nil. After
https://golang.org/issues/19348 is implemented the call/return overhead will be
also gone.
*/
package tracing
import (
"sync"
"sync/atomic"
"unsafe"
"lab.nexedi.com/kirr/neo/go/xcommon/tracing/internal/xruntime"
)
// big tracing lock
var traceMu sync.Mutex
var traceLocked int32 // for cheap protective checks whether Lock is held
// Lock serializes modification access to tracepoints.
//
// Under Lock it is safe to attach/detach probes to/from tracepoints:
// - no other goroutine is attaching or detaching probes from tracepoints,
// - a tracepoint readers won't be neither confused nor raced by such adjustments.
//
// Lock returns with the world stopped.
func Lock() {
traceMu.Lock()
xruntime.StopTheWorld("tracing lock")
atomic.StoreInt32(&traceLocked, 1)
// we synchronized with everyone via stopping the world - there is now
// no other goroutines running to race with.
xruntime.RaceIgnoreBegin()
}
// Unlock is the opposite to Lock and returns with the world resumed
func Unlock() {
xruntime.RaceIgnoreEnd()
atomic.StoreInt32(&traceLocked, 0)
xruntime.StartTheWorld()
traceMu.Unlock()
}
// verifyLocked makes sure tracing is locked and panics otherwise
func verifyLocked() {
if atomic.LoadInt32(&traceLocked) == 0 {
panic("tracing must be locked")
}
}
// verifyUnlocked makes sure tracing is not locked and panics otherwise
func verifyUnlocked() {
if atomic.LoadInt32(&traceLocked) != 0 {
panic("tracing must be unlocked")
}
}
// Probe describes one probe attached to a tracepoint
type Probe struct {
// NOTE .next must come first as probe list header is only 1 word and
// is treated as *Probe on probe attach/detach - accessing/modifying its .next
next, prev *Probe
// implicitly:
// probefunc func(some arguments)
}
// Next returns next probe attached to the same tracepoint.
//
// It is safe to iterate Next under any conditions.
func (p *Probe) Next() *Probe {
return p.next
}
// AttachProbe attaches newly created Probe to the end of a probe list.
//
// If group is non-nil the probe is also added to the group.
// Must be called under Lock.
// Probe must be newly created.
func AttachProbe(pg *ProbeGroup, listp **Probe, probe *Probe) {
verifyLocked()
if !(probe.prev == nil || probe.next == nil) {
panic("attach probe: probe is not newly created")
}
last := (*Probe)(unsafe.Pointer(listp))
for p := *listp; p != nil; last, p = p, p.next {
}
last.next = probe
probe.prev = last
if pg != nil {
pg.Add(probe)
}
}
// Detach detaches probe from a tracepoint.
//
// Must be called under Lock.
func (p *Probe) Detach() {
verifyLocked()
// protection: already detached
if p.prev == nil {
return
}
// we can safely change prev.next pointer:
// - no reader is currently reading it
// - either a reader already read prev.next, and will proceed with our probe entry, or
// - it will read updated prev.next and will proceed with p.next probe entry
p.prev.next = p.next
// we can safely change next.prev pointer:
// - readers only go through list forward
// - there is no other updater because we are under Lock
if p.next != nil {
p.next.prev = p.prev
}
// mark us detached so that if Detach is erroneously called the second
// time it does not do harm
p.prev = nil
p.next = nil
}
// ProbeGroup is a group of probes attached to tracepoints.
type ProbeGroup struct {
probev []*Probe
}
// Add adds a probe to the group.
//
// Must be called under Lock.
func (pg *ProbeGroup) Add(p *Probe) {
verifyLocked()
pg.probev = append(pg.probev, p)
}
// Done detaches all probes registered to the group.
//
// Must be called under normal conditions, not under Lock.
func (pg *ProbeGroup) Done() {
verifyUnlocked()
Lock()
defer Unlock()
for _, p := range pg.probev {
p.Detach()
}
pg.probev = nil
}
// Copyright (C) 2017 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
package tracing
import (
"reflect"
"runtime"
"testing"
"time"
"unsafe"
"github.com/kylelemons/godebug/pretty"
)
func TestAttachDetach(t *testing.T) {
var traceX *Probe // list head of a tracing event
// check that traceX probe list has such and such content and also that .prev
// pointers in all elements are right
checkX := func(probev ...*Probe) {
t.Helper()
var pv []*Probe
pp := (*Probe)(unsafe.Pointer(&traceX))
for p := traceX; p != nil; pp, p = p, p.next {
if p.prev != pp {
t.Fatalf("probe list: %#v: .prev is wrong", p)
}
pv = append(pv, p)
}
if !reflect.DeepEqual(pv, probev) {
t.Fatalf("probe list:\n%s\n", pretty.Compare(probev, pv))
}
}
checkX()
// attach probe to traceX
attachX := func(probe *Probe) {
Lock()
AttachProbe(nil, &traceX, probe)
Unlock()
}
// detach probe
detach := func(probe *Probe) {
Lock()
probe.Detach()
Unlock()
}
p1 := &Probe{}
attachX(p1)
checkX(p1)
detach(p1)
checkX()
detach(p1)
checkX()
attachX(p1)
checkX(p1)
p2 := &Probe{}
attachX(p2)
checkX(p1, p2)
p3 := &Probe{}
attachX(p3)
checkX(p1, p2, p3)
detach(p2)
checkX(p1, p3)
detach(p1)
checkX(p3)
detach(p3)
checkX()
}
// Test use vs concurent detach.
//
// Detach works under tracing lock (= world stopped) - so changing a probe list
// should be ok, but since race detector does not know we stopped the world it
// could complain.
func TestUseDetach(t *testing.T) {
var traceX *Probe // list head of a tracing event
// attach probe to traceX
probe := Probe{}
Lock()
AttachProbe(nil, &traceX, &probe)
Unlock()
// simulate traceX signalling and so probe usage and concurrent probe detach
go func() {
// delay a bit so that main goroutine first spins some time
// with non-empty probe list
time.Sleep(1 * time.Millisecond)
Lock()
probe.Detach()
Unlock()
}()
loop:
for {
np := 0
for p := traceX; p != nil; p = p.Next() {
np++
}
switch np {
case 1:
// ok - not yet detached
case 0:
// ok - detached
break loop
default:
t.Fatalf("probe seen %d times; must be either 1 or 0", np)
}
// XXX as of go19 tight loops are not preemptible (golang.org/issues/10958)
// and Lock does stop-the-world -> make this loop explicitly preemtible.
runtime.Gosched()
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment