// [Hash] hashes its arguments to compute an ID.
//
// 3. Enable each change that the pattern says should be enabled.
-// The [Matcher.Enable] method answers this question for a given change ID.
+// The [Matcher.ShouldEnable] method answers this question for a given change ID.
//
-// 4. Report each change that the pattern says should be reported.
-// The [Matcher.Report] method answers this question for a given change ID.
+// 4. Print a report identifying each change that the pattern says should be printed.
+// The [Matcher.ShouldPrint] method answers this question for a given change ID.
// The report consists of one more lines on standard error or standard output
// that contain a “match marker”. [Marker] returns the match marker for a given ID.
// When bisect reports a change as causing the failure, it identifies the change
-// by printing those report lines, with the match marker removed.
+// by printing the report lines with the match marker removed.
//
// # Example Usage
//
//
// Then, each time a potential change is considered, the program computes
// a change ID by hashing identifying information (source file and line, in this case)
-// and then calls m.ShouldEnable and m.ShouldReport to decide whether to
-// enable and report the change, respectively:
+// and then calls m.ShouldPrint and m.ShouldEnable to decide whether to
+// print and enable the change, respectively. The two can return different values
+// depending on whether bisect is trying to find a minimal set of changes to
+// disable or to enable to provoke the failure.
//
-// for each change {
+// It is usually helpful to write a helper function that accepts the identifying information
+// and then takes care of hashing, printing, and reporting whether the identified change
+// should be enabled. For example, a helper for changes identified by a file and line number
+// would be:
+//
+// func ShouldEnable(file string, line int) {
// h := bisect.Hash(file, line)
-// if m.ShouldEnable(h) {
-// enableChange()
-// }
-// if m.ShouldReport(h) {
-// log.Printf("%v %s:%d", bisect.Marker(h), file, line)
+// if m.ShouldPrint(h) {
+// fmt.Fprintf(os.Stderr, "%v %s:%d\n", bisect.Marker(h), file, line)
// }
+// return m.ShouldEnable(h)
// }
//
-// Note that the two return different values when bisect is searching for a
-// minimal set of changes to disable to provoke a failure.
-//
// Finally, note that New returns a nil Matcher when there is no pattern,
-// meaning that the target is not running under bisect at all.
+// meaning that the target is not running under bisect at all,
+// so all changes should be enabled and none should be printed.
// In that common case, the computation of the hash can be avoided entirely
// by checking for m == nil first:
//
-// for each change {
+// func ShouldEnable(file string, line int) bool {
// if m == nil {
-// enableChange()
-// } else {
-// h := bisect.Hash(file, line)
-// if m.ShouldEnable(h) {
-// enableChange()
-// }
-// if m.ShouldReport(h) {
-// log.Printf("%v %s:%d", bisect.Marker(h), file, line)
+// return false
+// }
+// h := bisect.Hash(file, line)
+// if m.ShouldPrint(h) {
+// fmt.Fprintf(os.Stderr, "%v %s:%d\n", bisect.Marker(h), file, line)
+// }
+// return m.ShouldEnable(h)
+// }
+//
+// When the identifying information is expensive to format, this code can call
+// [Matcher.MarkerOnly] to find out whether short report lines containing only the
+// marker are permitted for a given run. (Bisect permits such lines when it is
+// still exploring the space of possible changes and will not be showing the
+// output to the user.) If so, the client can choose to print only the marker:
+//
+// func ShouldEnable(file string, line int) bool {
+// if m == nil {
+// return false
+// }
+// h := bisect.Hash(file, line)
+// if m.ShouldPrint(h) {
+// if m.MarkerOnly() {
+// bisect.PrintMarker(os.Stderr)
+// } else {
+// fmt.Fprintf(os.Stderr, "%v %s:%d\n", bisect.Marker(h), file, line)
// }
// }
+// return m.ShouldEnable(h)
// }
//
+// This specific helper – deciding whether to enable a change identified by
+// file and line number and printing about the change when necessary – is
+// provided by the [Matcher.FileLine] method.
+//
+// Another common usage is deciding whether to make a change in a function
+// based on the caller's stack, to identify the specific calling contexts that the
+// change breaks. The [Matcher.Stack] method takes care of obtaining the stack,
+// printing it when necessary, and reporting whether to enable the change
+// based on that stack.
+//
// # Pattern Syntax
//
// Patterns are generated by the bisect tool and interpreted by [New].
// enabled but fails with no changes enabled. In this case, bisect
// searches for minimal sets of changes to disable.
// Put another way, the leading “!” inverts the result from [Matcher.ShouldEnable]
-// but does not invert the result from [Matcher.ShouldReport].
+// but does not invert the result from [Matcher.ShouldPrint].
//
// As a convenience for manual debugging, “n” is an alias for “!y”,
// meaning to disable and report all changes.
// in most runs.
package bisect
+import (
+ "runtime"
+ "sync"
+ "sync/atomic"
+ "unsafe"
+)
+
// New creates and returns a new Matcher implementing the given pattern.
// The pattern syntax is defined in the package doc comment.
//
// In addition to the pattern syntax syntax, New("") returns nil, nil.
// The nil *Matcher is valid for use: it returns true from ShouldEnable
-// and false from ShouldReport for all changes. Callers can avoid calling
+// and false from ShouldPrint for all changes. Callers can avoid calling
// [Hash], [Matcher.ShouldEnable], and [Matcher.ShouldPrint] entirely
// when they recognize the nil Matcher.
func New(pattern string) (*Matcher, error) {
verbose bool
enable bool // when true, list is for “enable and report” (when false, “disable and report”)
list []cond // conditions; later ones win over earlier ones
+ dedup atomicPointerDedup
+}
+
+// atomicPointerDedup is an atomic.Pointer[dedup],
+// but we are avoiding using Go 1.19's atomic.Pointer
+// until the bootstrap toolchain can be relied upon to have it.
+type atomicPointerDedup struct {
+ p unsafe.Pointer
+}
+
+func (p *atomicPointerDedup) Load() *dedup {
+ return (*dedup)(atomic.LoadPointer(&p.p))
+}
+
+func (p *atomicPointerDedup) CompareAndSwap(old, new *dedup) bool {
+ return atomic.CompareAndSwapPointer(&p.p, unsafe.Pointer(old), unsafe.Pointer(new))
}
// A cond is a single condition in the matcher.
result bool
}
-// Verbose reports whether the reports will be shown to users
-// and need to include a human-readable change description.
-// If not, the target can print just the Marker on a line by itself
-// and perhaps save some computation.
-func (m *Matcher) Verbose() bool {
- return m.verbose
+// MarkerOnly reports whether it is okay to print only the marker for
+// a given change, omitting the identifying information.
+// MarkerOnly returns true when bisect is using the printed reports
+// only for an intermediate search step, not for showing to users.
+func (m *Matcher) MarkerOnly() bool {
+ return !m.verbose
}
// ShouldEnable reports whether the change with the given id should be enabled.
return false == m.enable
}
-// ShouldReport reports whether the change with the given id should be reported.
-func (m *Matcher) ShouldReport(id uint64) bool {
+// ShouldPrint reports whether to print identifying information about the change with the given id.
+func (m *Matcher) ShouldPrint(id uint64) bool {
if m == nil {
return false
}
return false
}
+// FileLine reports whether the change identified by file and line should be enabled.
+// If the change should be printed, FileLine prints a one-line report to w.
+func (m *Matcher) FileLine(w Writer, file string, line int) bool {
+ if m == nil {
+ return true
+ }
+ return m.fileLine(w, file, line)
+}
+
+// fileLine does the real work for FileLine.
+// This lets FileLine's body handle m == nil and potentially be inlined.
+func (m *Matcher) fileLine(w Writer, file string, line int) bool {
+ h := Hash(file, line)
+ if m.ShouldPrint(h) {
+ if m.MarkerOnly() {
+ PrintMarker(w, h)
+ } else {
+ printFileLine(w, h, file, line)
+ }
+ }
+ return m.ShouldEnable(h)
+}
+
+// printFileLine prints a non-marker-only report for file:line to w.
+func printFileLine(w Writer, h uint64, file string, line int) error {
+ const markerLen = 40 // overestimate
+ b := make([]byte, 0, markerLen+len(file)+24)
+ b = AppendMarker(b, h)
+ b = appendFileLine(b, file, line)
+ b = append(b, '\n')
+ _, err := w.Write(b)
+ return err
+}
+
+// appendFileLine appends file:line to dst, returning the extended slice.
+func appendFileLine(dst []byte, file string, line int) []byte {
+ dst = append(dst, file...)
+ dst = append(dst, ':')
+ u := uint(line)
+ if line < 0 {
+ dst = append(dst, '-')
+ u = -u
+ }
+ var buf [24]byte
+ i := len(buf)
+ for i == len(buf) || u > 0 {
+ i--
+ buf[i] = '0' + byte(u%10)
+ u /= 10
+ }
+ dst = append(dst, buf[i:]...)
+ return dst
+}
+
+// MatchStack assigns the current call stack a change ID.
+// If the stack should be printed, MatchStack prints it.
+// Then MatchStack reports whether a change at the current call stack should be enabled.
+func (m *Matcher) Stack(w Writer) bool {
+ if m == nil {
+ return true
+ }
+ return m.stack(w)
+}
+
+// stack does the real work for Stack.
+// This lets stack's body handle m == nil and potentially be inlined.
+func (m *Matcher) stack(w Writer) bool {
+ const maxStack = 16
+ var stk [maxStack]uintptr
+ n := runtime.Callers(3, stk[:])
+ if n == 0 {
+ return false
+ }
+
+ h := Hash(stk[:n])
+ if m.ShouldPrint(h) {
+ var d *dedup
+ for {
+ d = m.dedup.Load()
+ if d != nil {
+ break
+ }
+ d = new(dedup)
+ if m.dedup.CompareAndSwap(nil, d) {
+ break
+ }
+ }
+
+ if m.MarkerOnly() {
+ if !d.seenLossy(h) {
+ PrintMarker(w, h)
+ }
+ } else {
+ if !d.seen(h) {
+ printStack(w, h, stk[:n])
+ }
+ }
+ }
+ return m.ShouldEnable(h)
+
+}
+
+// Writer is the same interface as io.Writer.
+// It is duplicated here to avoid importing io.
+type Writer interface {
+ Write([]byte) (int, error)
+}
+
+// PrintMarker prints to w a one-line report containing only the marker for h.
+// It is appropriate to use when [Matcher.ShouldPrint] and [Matcher.MarkerOnly] both return true.
+func PrintMarker(w Writer, h uint64) error {
+ var buf [50]byte
+ b := AppendMarker(buf[:], h)
+ b = append(b, '\n')
+ _, err := w.Write(b)
+ return err
+}
+
+// printStack prints to w a multi-line report containing a formatting of the call stack stk,
+// with each line preceded by the marker for h.
+func printStack(w Writer, h uint64, stk []uintptr) error {
+ buf := make([]byte, 0, 2048)
+
+ var prefixBuf [100]byte
+ prefix := AppendMarker(prefixBuf[:0], h)
+
+ frames := runtime.CallersFrames(stk)
+ for {
+ f, more := frames.Next()
+ buf = append(buf, prefix...)
+ buf = append(buf, f.Func.Name()...)
+ buf = append(buf, "()\n"...)
+ buf = append(buf, prefix...)
+ buf = append(buf, '\t')
+ buf = appendFileLine(buf, f.File, f.Line)
+ buf = append(buf, '\n')
+ if !more {
+ break
+ }
+ }
+ buf = append(buf, prefix...)
+ buf = append(buf, '\n')
+ _, err := w.Write(buf)
+ return err
+}
+
// Marker returns the match marker text to use on any line reporting details
// about a match of the given ID.
// It always returns the hexadecimal format.
// Trivial error implementation, here to avoid importing errors.
+// parseError is a trivial error implementation,
+// defined here to avoid importing errors.
type parseError struct{ text string }
func (e *parseError) Error() string { return e.text }
// FNV-1a implementation. See Go's hash/fnv/fnv.go.
-// Copied here for simplicity (can handle uints directly)
-// and to avoid the dependency.
+// Copied here for simplicity (can handle integers more directly)
+// and to avoid importing hash/fnv.
const (
offset64 uint64 = 14695981039346656037
}
return h
}
+
+// A dedup is a deduplicator for call stacks, so that we only print
+// a report for new call stacks, not for call stacks we've already
+// reported.
+//
+// It has two modes: an approximate but lock-free mode that
+// may still emit some duplicates, and a precise mode that uses
+// a lock and never emits duplicates.
+type dedup struct {
+ // 128-entry 4-way, lossy cache for seenLossy
+ recent [128][4]uint64
+
+ // complete history for seen
+ mu sync.Mutex
+ m map[uint64]bool
+}
+
+// seen records that h has now been seen and reports whether it was seen before.
+// When seen returns false, the caller is expected to print a report for h.
+func (d *dedup) seen(h uint64) bool {
+ d.mu.Lock()
+ if d.m == nil {
+ d.m = make(map[uint64]bool)
+ }
+ seen := d.m[h]
+ d.m[h] = true
+ d.mu.Unlock()
+ return seen
+}
+
+// seenLossy is a variant of seen that avoids a lock by using a cache of recently seen hashes.
+// Each cache entry is N-way set-associative: h can appear in any of the slots.
+// If h does not appear in any of them, then it is inserted into a random slot,
+// overwriting whatever was there before.
+func (d *dedup) seenLossy(h uint64) bool {
+ cache := &d.recent[uint(h)%uint(len(d.recent))]
+ for i := 0; i < len(cache); i++ {
+ if atomic.LoadUint64(&cache[i]) == h {
+ return true
+ }
+ }
+
+ // Compute index in set to evict as hash of current set.
+ ch := offset64
+ for _, x := range cache {
+ ch = fnvUint64(ch, x)
+ }
+ atomic.StoreUint64(&cache[uint(ch)%uint(len(cache))], h)
+ return false
+}