github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/stats.go (about)

     1  // Copyright 2022 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  package main
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"time"
    11  
    12  	"github.com/google/syzkaller/dashboard/dashapi"
    13  	"github.com/google/syzkaller/pkg/stats/syzbotstats"
    14  	"google.golang.org/appengine/v2"
    15  	db "google.golang.org/appengine/v2/datastore"
    16  )
    17  
    18  // bugInput structure contains the information for collecting all bug-related statistics.
    19  type bugInput struct {
    20  	bug           *Bug
    21  	bugReporting  *BugReporting
    22  	reportedCrash *Crash
    23  	build         *Build
    24  }
    25  
    26  func (bi *bugInput) fixedAt() time.Time {
    27  	closeTime := time.Time{}
    28  	if bi.bug.Status == BugStatusFixed {
    29  		closeTime = bi.bug.Closed
    30  	}
    31  	for _, commit := range bi.bug.CommitInfo {
    32  		if closeTime.IsZero() || closeTime.After(commit.Date) {
    33  			closeTime = commit.Date
    34  		}
    35  	}
    36  	return closeTime
    37  }
    38  
    39  func (bi *bugInput) bugStatus() (syzbotstats.BugStatus, error) {
    40  	if bi.bug.Status == BugStatusFixed ||
    41  		bi.bug.Closed.IsZero() && len(bi.bug.Commits) > 0 {
    42  		return syzbotstats.BugFixed, nil
    43  	} else if bi.bug.Closed.IsZero() {
    44  		return syzbotstats.BugPending, nil
    45  	} else if bi.bug.Status == BugStatusDup {
    46  		return syzbotstats.BugDup, nil
    47  	} else if bi.bug.Status == BugStatusInvalid {
    48  		if bi.bugReporting.Auto {
    49  			return syzbotstats.BugAutoInvalidated, nil
    50  		} else {
    51  			return syzbotstats.BugInvalidated, nil
    52  		}
    53  	}
    54  	return "", fmt.Errorf("cannot determine status")
    55  }
    56  
    57  // allBugInputs queries the raw data about all bugs from a namespace.
    58  func allBugInputs(c context.Context, ns string) ([]*bugInput, error) {
    59  	filter := func(query *db.Query) *db.Query {
    60  		return query.Filter("Namespace=", ns)
    61  	}
    62  	inputs := []*bugInput{}
    63  	bugs, bugKeys, err := loadAllBugs(c, filter)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	crashKeys := []*db.Key{}
    69  	crashToInput := map[*db.Key]*bugInput{}
    70  	for i, bug := range bugs {
    71  		bugReporting := lastReportedReporting(bug)
    72  		input := &bugInput{
    73  			bug:          bug,
    74  			bugReporting: bugReporting,
    75  		}
    76  		if bugReporting.CrashID != 0 {
    77  			crashKey := db.NewKey(c, "Crash", "", bugReporting.CrashID, bugKeys[i])
    78  			crashKeys = append(crashKeys, crashKey)
    79  			crashToInput[crashKey] = input
    80  		}
    81  		inputs = append(inputs, input)
    82  	}
    83  	// Fetch crashes.
    84  	buildKeys := []*db.Key{}
    85  	buildToInput := map[*db.Key]*bugInput{}
    86  	if len(crashKeys) > 0 {
    87  		crashes := make([]*Crash, len(crashKeys))
    88  		if badKey, err := getAllMulti(c, crashKeys, crashes); err != nil {
    89  			return nil, fmt.Errorf("failed to fetch crashes for %v: %w", badKey, err)
    90  		}
    91  		for i, crash := range crashes {
    92  			if crash == nil {
    93  				continue
    94  			}
    95  			input := crashToInput[crashKeys[i]]
    96  			input.reportedCrash = crash
    97  
    98  			buildKey := buildKey(c, ns, crash.BuildID)
    99  			buildKeys = append(buildKeys, buildKey)
   100  			buildToInput[buildKey] = input
   101  		}
   102  	}
   103  	// Fetch builds.
   104  	if len(buildKeys) > 0 {
   105  		builds := make([]*Build, len(buildKeys))
   106  		if badKey, err := getAllMulti(c, buildKeys, builds); err != nil {
   107  			return nil, fmt.Errorf("failed to fetch builds for %v: %w", badKey, err)
   108  		}
   109  		for i, build := range builds {
   110  			if build != nil {
   111  				buildToInput[buildKeys[i]].build = build
   112  			}
   113  		}
   114  	}
   115  	return inputs, nil
   116  }
   117  
   118  // Circumventing the datastore's multi query limitation.
   119  func getAllMulti[T any](c context.Context, keys []*db.Key, objects []*T) (*db.Key, error) {
   120  	const step = 1000
   121  	for from := 0; from < len(keys); from += step {
   122  		to := from + step
   123  		if to > len(keys) {
   124  			to = len(keys)
   125  		}
   126  		err := db.GetMulti(c, keys[from:to], objects[from:to])
   127  		if err == nil {
   128  			continue
   129  		}
   130  		var merr appengine.MultiError
   131  		if errors.As(err, &merr) {
   132  			for i, objErr := range merr {
   133  				if objErr != nil {
   134  					return keys[from+i], objErr
   135  				}
   136  			}
   137  		}
   138  		return nil, err
   139  	}
   140  	return nil, nil
   141  }
   142  
   143  // getBugSummaries extracts the list of BugStatSummary objects among bugs
   144  // that reached the specific reporting stage.
   145  func getBugSummaries(c context.Context, ns, stage string) ([]*syzbotstats.BugStatSummary, error) {
   146  	inputs, err := allBugInputs(c, ns)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  	var ret []*syzbotstats.BugStatSummary
   151  	for _, input := range inputs {
   152  		bug, crash := input.bug, input.reportedCrash
   153  		if crash == nil {
   154  			continue
   155  		}
   156  		targetStage := bugReportingByName(bug, stage)
   157  		if targetStage == nil || targetStage.Reported.IsZero() {
   158  			continue
   159  		}
   160  		obj := &syzbotstats.BugStatSummary{
   161  			Title:        bug.Title,
   162  			FirstTime:    bug.FirstTime,
   163  			ReleasedTime: targetStage.Reported,
   164  			ResolvedTime: bug.Closed,
   165  			HappenedOn:   bug.HappenedOn,
   166  			Strace:       dashapi.CrashFlags(crash.Flags)&dashapi.CrashUnderStrace > 0,
   167  		}
   168  		for _, stage := range bug.Reporting {
   169  			if stage.ID != "" {
   170  				obj.IDs = append(obj.IDs, stage.ID)
   171  			}
   172  		}
   173  		for _, commit := range bug.CommitInfo {
   174  			obj.FixHashes = append(obj.FixHashes, commit.Hash)
   175  		}
   176  		if crash.ReproSyz > 0 {
   177  			obj.ReproTime = crash.Time
   178  		}
   179  		if bug.BisectCause == BisectYes {
   180  			causeBisect, err := queryBestBisection(c, bug, JobBisectCause)
   181  			if err != nil {
   182  				return nil, err
   183  			}
   184  			if causeBisect != nil {
   185  				obj.CauseBisectTime = causeBisect.job.Finished
   186  			}
   187  		}
   188  		fixTime := input.fixedAt()
   189  		if !fixTime.IsZero() && (obj.ResolvedTime.IsZero() || fixTime.Before(obj.ResolvedTime)) {
   190  			// Take the date of the fixing commit, if it's earlier.
   191  			obj.ResolvedTime = fixTime
   192  		}
   193  		obj.Status, err = input.bugStatus()
   194  		if err != nil {
   195  			return nil, fmt.Errorf("%s: %w", bug.Title, err)
   196  		}
   197  
   198  		const minAvgHitCrashes = 5
   199  		const minAvgHitPeriod = time.Hour * 24
   200  		if bug.NumCrashes >= minAvgHitCrashes ||
   201  			bug.LastTime.Sub(bug.FirstTime) < minAvgHitPeriod {
   202  			// If there are only a few crashes or they all happened within a single day,
   203  			// it's hard to make any accurate frequency estimates.
   204  			timeSpan := bug.LastTime.Sub(bug.FirstTime)
   205  			obj.HitsPerDay = float64(bug.NumCrashes) / (timeSpan.Hours() / 24)
   206  		}
   207  
   208  		for _, label := range bug.LabelValues(SubsystemLabel) {
   209  			obj.Subsystems = append(obj.Subsystems, label.Value)
   210  		}
   211  
   212  		ret = append(ret, obj)
   213  	}
   214  	return ret, nil
   215  }