github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/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/stat/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  	crashLoader := &dependencyLoader[Crash]{}
    68  	for i, bug := range bugs {
    69  		bugReporting := lastReportedReporting(bug)
    70  		input := &bugInput{
    71  			bug:          bug,
    72  			bugReporting: bugReporting,
    73  		}
    74  		if bugReporting.CrashID != 0 {
    75  			crashKey := db.NewKey(c, "Crash", "", bugReporting.CrashID, bugKeys[i])
    76  			crashLoader.add(crashKey, func(crash *Crash) {
    77  				input.reportedCrash = crash
    78  			})
    79  		}
    80  		inputs = append(inputs, input)
    81  	}
    82  	if err := crashLoader.load(c); err != nil {
    83  		return nil, fmt.Errorf("failed to fetch crashes: %w", err)
    84  	}
    85  	buildLoader := &dependencyLoader[Build]{}
    86  	for _, input := range inputs {
    87  		if input.reportedCrash == nil {
    88  			continue
    89  		}
    90  		buildLoader.add(buildKey(c, ns, input.reportedCrash.BuildID), func(build *Build) {
    91  			input.build = build
    92  		})
    93  	}
    94  	if err := buildLoader.load(c); err != nil {
    95  		return nil, fmt.Errorf("failed to fetch builds: %w", err)
    96  	}
    97  	return inputs, nil
    98  }
    99  
   100  // Circumventing the datastore's multi query limitation.
   101  func getAllMulti[T any](c context.Context, keys []*db.Key, objects []*T) (*db.Key, error) {
   102  	const step = 1000
   103  	for from := 0; from < len(keys); from += step {
   104  		to := min(from+step, len(keys))
   105  		err := db.GetMulti(c, keys[from:to], objects[from:to])
   106  		if err == nil {
   107  			continue
   108  		}
   109  		var merr appengine.MultiError
   110  		if errors.As(err, &merr) {
   111  			for i, objErr := range merr {
   112  				if objErr != nil {
   113  					return keys[from+i], objErr
   114  				}
   115  			}
   116  		}
   117  		return nil, err
   118  	}
   119  	return nil, nil
   120  }
   121  
   122  // getBugSummaries extracts the list of BugStatSummary objects among bugs
   123  // that reached the specific reporting stage.
   124  func getBugSummaries(c context.Context, ns, stage string) ([]*syzbotstats.BugStatSummary, error) {
   125  	inputs, err := allBugInputs(c, ns)
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  	var ret []*syzbotstats.BugStatSummary
   130  	for _, input := range inputs {
   131  		bug, crash := input.bug, input.reportedCrash
   132  		if crash == nil {
   133  			continue
   134  		}
   135  		targetStage := bugReportingByName(bug, stage)
   136  		if targetStage == nil || targetStage.Reported.IsZero() {
   137  			continue
   138  		}
   139  		obj := &syzbotstats.BugStatSummary{
   140  			Title:        bug.Title,
   141  			FirstTime:    bug.FirstTime,
   142  			ReleasedTime: targetStage.Reported,
   143  			ResolvedTime: bug.Closed,
   144  			HappenedOn:   bug.HappenedOn,
   145  			Strace:       dashapi.CrashFlags(crash.Flags)&dashapi.CrashUnderStrace > 0,
   146  		}
   147  		for _, stage := range bug.Reporting {
   148  			if stage.ID != "" {
   149  				obj.IDs = append(obj.IDs, stage.ID)
   150  			}
   151  		}
   152  		for _, commit := range bug.CommitInfo {
   153  			obj.FixHashes = append(obj.FixHashes, commit.Hash)
   154  		}
   155  		if crash.ReproSyz > 0 {
   156  			obj.ReproTime = crash.Time
   157  		}
   158  		if bug.BisectCause == BisectYes {
   159  			causeBisect, err := queryBestBisection(c, bug, JobBisectCause)
   160  			if err != nil {
   161  				return nil, err
   162  			}
   163  			if causeBisect != nil {
   164  				obj.CauseBisectTime = causeBisect.job.Finished
   165  			}
   166  		}
   167  		fixTime := input.fixedAt()
   168  		if !fixTime.IsZero() && (obj.ResolvedTime.IsZero() || fixTime.Before(obj.ResolvedTime)) {
   169  			// Take the date of the fixing commit, if it's earlier.
   170  			obj.ResolvedTime = fixTime
   171  		}
   172  		obj.Status, err = input.bugStatus()
   173  		if err != nil {
   174  			return nil, fmt.Errorf("%s: %w", bug.Title, err)
   175  		}
   176  
   177  		const minAvgHitCrashes = 5
   178  		const minAvgHitPeriod = time.Hour * 24
   179  		if bug.NumCrashes >= minAvgHitCrashes ||
   180  			bug.LastTime.Sub(bug.FirstTime) < minAvgHitPeriod {
   181  			// If there are only a few crashes or they all happened within a single day,
   182  			// it's hard to make any accurate frequency estimates.
   183  			timeSpan := bug.LastTime.Sub(bug.FirstTime)
   184  			obj.HitsPerDay = float64(bug.NumCrashes) / (timeSpan.Hours() / 24)
   185  		}
   186  
   187  		for _, label := range bug.LabelValues(SubsystemLabel) {
   188  			obj.Subsystems = append(obj.Subsystems, label.Value)
   189  		}
   190  
   191  		ret = append(ret, obj)
   192  	}
   193  	return ret, nil
   194  }