| package regression |
| |
| import ( |
| "database/sql" |
| "encoding/json" |
| "fmt" |
| |
| "go.skia.org/infra/go/util" |
| "go.skia.org/infra/perf/go/cid" |
| "go.skia.org/infra/perf/go/clustering2" |
| "go.skia.org/infra/perf/go/dataframe" |
| "go.skia.org/infra/perf/go/db" |
| ) |
| |
| // Subset is the subset of regressions we are querying for. |
| type Subset string |
| |
| const ( |
| ALL_SUBSET Subset = "all" // Include all regressions in a range. |
| REGRESSIONS_SUBSET Subset = "regressions" // Only include regressions in a range that are alerting. |
| UNTRIAGED_SUBSET Subset = "untriaged" // All untriaged alerting regressions regardless of range. |
| ) |
| |
| // Store persists Regressions to/from an SQL database. |
| type Store struct { |
| } |
| |
| // NewStore returns a new Store. |
| func NewStore() *Store { |
| return &Store{} |
| } |
| |
| // load Regressions stored for the given commit. |
| func (s *Store) load(tx *sql.Tx, cid *cid.CommitDetail) (*Regressions, error) { |
| row := tx.QueryRow("SELECT cid, body FROM regression WHERE cid=?", cid.ID()) |
| if row == nil { |
| return nil, fmt.Errorf("Failed to query database for %q.", cid.ID()) |
| } |
| var id string |
| var body string |
| if err := row.Scan(&id, &body); err != nil { |
| return nil, fmt.Errorf("Failed to read from database: %s", err) |
| } |
| ret := New() |
| |
| if err := json.Unmarshal([]byte(body), ret); err != nil { |
| return nil, fmt.Errorf("Failed to decode JSON body: %s", err) |
| } |
| return ret, nil |
| } |
| |
| // store Regressions for the given commit. |
| func (s *Store) store(tx *sql.Tx, cid *cid.CommitDetail, r *Regressions) error { |
| body, err := r.JSON() |
| // MEDIUMTEXT is only 16MB, and will silently be truncated. |
| if len(body) > 16777215 { |
| return fmt.Errorf("Regressions is too large, >16 MB.") |
| } |
| if err != nil { |
| return fmt.Errorf("Failed to encode Regressions to JSON: %s", err) |
| } |
| _, err = tx.Exec("INSERT INTO regression (cid, timestamp, triaged, body) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE cid=?, timestamp=?, triaged=?, body=?", |
| cid.ID(), cid.Timestamp, r.Triaged(), body, |
| cid.ID(), cid.Timestamp, r.Triaged(), body) |
| |
| if err != nil { |
| return fmt.Errorf("Failed to write to database: %s", err) |
| } |
| return nil |
| } |
| |
| // Untriaged returns the number of untriaged regressions. |
| func (s *Store) Untriaged() (int, error) { |
| row := db.DB.QueryRow("SELECT count(*) FROM regression WHERE triaged=false") |
| if row == nil { |
| return -1, fmt.Errorf("Failed to query database for count of untriaged regressions.") |
| } |
| var count int |
| if err := row.Scan(&count); err != nil { |
| return -1, fmt.Errorf("Failed to read from database: %s", err) |
| } |
| return count, nil |
| } |
| |
| // Range returns a map from cid.ID()'s to *Regressions that exist in the given time range. |
| func (s *Store) Range(begin, end int64, subset Subset) (map[string]*Regressions, error) { |
| ret := map[string]*Regressions{} |
| |
| rows, err := db.DB.Query("SELECT cid, timestamp, body FROM regression WHERE timestamp >= ? AND timestamp < ? ORDER BY timestamp", begin, end) |
| if subset == UNTRIAGED_SUBSET { |
| rows, err = db.DB.Query("SELECT cid, timestamp, body FROM regression WHERE triaged=false ORDER BY timestamp") |
| } |
| if err != nil { |
| return nil, fmt.Errorf("Failed to query from database: %s", err) |
| } |
| defer util.Close(rows) |
| for rows.Next() { |
| var id string |
| var timestamp int64 |
| var body string |
| if err := rows.Scan(&id, ×tamp, &body); err != nil { |
| return nil, fmt.Errorf("Failed to read from database: %s", err) |
| } |
| reg := New() |
| if err := json.Unmarshal([]byte(body), reg); err != nil { |
| return nil, fmt.Errorf("Failed to decode JSON body: %s", err) |
| } |
| ret[id] = reg |
| } |
| if err := rows.Err(); err != nil { |
| return nil, fmt.Errorf("Error while iterating rows: %s", err) |
| } |
| return ret, nil |
| } |
| |
| // intx runs f within a database transaction. |
| // |
| func intx(f func(tx *sql.Tx) error) (err error) { |
| tx, err := db.DB.Begin() |
| if err != nil { |
| return fmt.Errorf("Failed to start transaction: %s", err) |
| } |
| |
| defer func() { |
| if err != nil { |
| _ = tx.Rollback() |
| return |
| } |
| err = tx.Commit() |
| }() |
| |
| err = f(tx) |
| return err |
| } |
| |
| // SetHigh sets the cluster for a high regression at the given commit and query. |
| func (s *Store) SetHigh(cid *cid.CommitDetail, query string, df *dataframe.FrameResponse, high *clustering2.ClusterSummary) error { |
| return intx(func(tx *sql.Tx) error { |
| r, err := s.load(tx, cid) |
| if err != nil { |
| r = New() |
| } |
| r.SetHigh(query, df, high) |
| return s.store(tx, cid, r) |
| }) |
| } |
| |
| // SetLow sets the cluster for a low regression at the given commit and query. |
| func (s *Store) SetLow(cid *cid.CommitDetail, query string, df *dataframe.FrameResponse, low *clustering2.ClusterSummary) error { |
| return intx(func(tx *sql.Tx) error { |
| r, err := s.load(tx, cid) |
| if err != nil { |
| r = New() |
| } |
| r.SetLow(query, df, low) |
| return s.store(tx, cid, r) |
| }) |
| } |
| |
| // TriageLow sets the triage status for the low cluster at the given commit and query. |
| func (s *Store) TriageLow(cid *cid.CommitDetail, query string, tr TriageStatus) error { |
| return intx(func(tx *sql.Tx) error { |
| r, err := s.load(tx, cid) |
| if err != nil { |
| return fmt.Errorf("Failed to load Regressions: %s", err) |
| } |
| if err = r.TriageLow(query, tr); err != nil { |
| return fmt.Errorf("Failed to update Regressions: %s", err) |
| } |
| return s.store(tx, cid, r) |
| }) |
| } |
| |
| // TriageHigh sets the triage status for the high cluster at the given commit and query. |
| func (s *Store) TriageHigh(cid *cid.CommitDetail, query string, tr TriageStatus) error { |
| return intx(func(tx *sql.Tx) error { |
| r, err := s.load(tx, cid) |
| if err != nil { |
| return fmt.Errorf("Failed to load Regressions: %s", err) |
| } |
| if err := r.TriageHigh(query, tr); err != nil { |
| return fmt.Errorf("Failed to update Regressions: %s", err) |
| } |
| return s.store(tx, cid, r) |
| }) |
| } |