go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/changepoints/inputbuffer/input_buffer.go (about)

     1  // Copyright 2023 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package inputbuffer handles the input buffer of change point analysis.
    16  package inputbuffer
    17  
    18  import (
    19  	"bytes"
    20  	"encoding/binary"
    21  	"fmt"
    22  	"time"
    23  
    24  	"go.chromium.org/luci/common/errors"
    25  
    26  	"go.chromium.org/luci/analysis/internal/span"
    27  )
    28  
    29  const (
    30  	// The version of the encoding to encode the verdict history.
    31  	EncodingVersion = 2
    32  	// Capacity of the hot buffer, i.e. how many verdicts it can hold.
    33  	DefaultHotBufferCapacity = 100
    34  	// Capacity of the cold buffer, i.e. how many verdicts it can hold.
    35  	DefaultColdBufferCapacity = 2000
    36  	// VerdictsInsertedHint is the number of verdicts expected
    37  	// to be inserted in one usage of the input buffer. Buffers
    38  	// will be allocated assuming this is the maximum number
    39  	// inserted, if the actual number is higher, it may trigger
    40  	// additional memory allocations.
    41  	//
    42  	// Currently a value of 1 is used as ingestion process ingests one
    43  	// invocation at a time.
    44  	VerdictsInsertedHint = 1
    45  )
    46  
    47  type Buffer struct {
    48  	// Capacity of the hot buffer. If it is full, the content will be written
    49  	// into the cold buffer.
    50  	HotBufferCapacity int
    51  	HotBuffer         History
    52  	// Capacity of the cold buffer.
    53  	ColdBufferCapacity int
    54  	ColdBuffer         History
    55  	// IsColdBufferDirty will be set to 1 if the cold buffer is dirty.
    56  	// This means we need to write the cold buffer to Spanner.
    57  	IsColdBufferDirty bool
    58  }
    59  
    60  type History struct {
    61  	// Verdicts, sorted by commit position (oldest first), and
    62  	// then result time (oldest first).
    63  	Verdicts []PositionVerdict
    64  }
    65  
    66  type PositionVerdict struct {
    67  	// The commit position for the verdict.
    68  	CommitPosition int
    69  	// Denotes whether this verdict is a simple expected pass verdict or not.
    70  	// A simple expected pass verdict has only one test result, which is expected pass.
    71  	IsSimpleExpectedPass bool
    72  	// The partition time that this PositionVerdict was ingested.
    73  	// When stored, it is truncated to the nearest hour.
    74  	Hour time.Time
    75  	// The details of the verdict.
    76  	Details VerdictDetails
    77  }
    78  
    79  type VerdictDetails struct {
    80  	// Whether a verdict is exonerated or not.
    81  	IsExonerated bool
    82  	// Details of the runs in the verdict.
    83  	Runs []Run
    84  }
    85  
    86  type ResultCounts struct {
    87  	// Number of passed result.
    88  	PassCount int
    89  	// Number of failed result.
    90  	FailCount int
    91  	// Number of crashed result.
    92  	CrashCount int
    93  	// Number of aborted result.
    94  	AbortCount int
    95  }
    96  
    97  type Run struct {
    98  	// Counts for expected results.
    99  	Expected ResultCounts
   100  	// Counts for unexpected results.
   101  	Unexpected ResultCounts
   102  	// Whether this run is a duplicate run.
   103  	IsDuplicate bool
   104  }
   105  
   106  func (r ResultCounts) Count() int {
   107  	return r.PassCount + r.FailCount + r.CrashCount + r.AbortCount
   108  }
   109  
   110  type MergedInputBuffer struct {
   111  	inputBuffer *Buffer
   112  	// Buffer contains the merged verdicts.
   113  	Buffer History
   114  }
   115  
   116  // New allocates an empty input buffer with default capacity.
   117  func New() *Buffer {
   118  	return NewWithCapacity(DefaultHotBufferCapacity, DefaultColdBufferCapacity)
   119  }
   120  
   121  // NewWithCapacity allocates an empty input buffer with the given capacity.
   122  func NewWithCapacity(hotBufferCapacity, coldBufferCapacity int) *Buffer {
   123  	return &Buffer{
   124  		HotBufferCapacity: hotBufferCapacity,
   125  		// HotBufferCapacity is a hard limit on the number of verdicts
   126  		// stored in Spanner, but only a soft limit during processing.
   127  		// After new verdicts are ingested, but before eviction is
   128  		// considered, the limit can be exceeded.
   129  		HotBuffer:          History{Verdicts: make([]PositionVerdict, 0, hotBufferCapacity+VerdictsInsertedHint)},
   130  		ColdBufferCapacity: coldBufferCapacity,
   131  		// ColdBufferCapacity is a hard limit on the number of verdicts
   132  		// stored in Spanner, but only a soft limit during processing.
   133  		// After new verdicts are ingested, but before eviction is
   134  		// considered, the limit can be exceeded (due to the new
   135  		// verdicts or due to compaction from hot buffer to cold buffer).
   136  		ColdBuffer: History{Verdicts: make([]PositionVerdict, 0, coldBufferCapacity+hotBufferCapacity+VerdictsInsertedHint)},
   137  	}
   138  }
   139  
   140  // Copy makes a deep copy of the input buffer.
   141  func (ib *Buffer) Copy() *Buffer {
   142  	return &Buffer{
   143  		HotBufferCapacity:  ib.HotBufferCapacity,
   144  		HotBuffer:          ib.HotBuffer.Copy(),
   145  		ColdBufferCapacity: ib.ColdBufferCapacity,
   146  		ColdBuffer:         ib.ColdBuffer.Copy(),
   147  		IsColdBufferDirty:  ib.IsColdBufferDirty,
   148  	}
   149  }
   150  
   151  // Copy makes a deep copy of the History.
   152  func (h History) Copy() History {
   153  	// Make a deep copy of verdicts.
   154  	verdictsCopy := make([]PositionVerdict, len(h.Verdicts), cap(h.Verdicts))
   155  	copy(verdictsCopy, h.Verdicts)
   156  
   157  	// Including the nested runs slice.
   158  	for i, v := range verdictsCopy {
   159  		if v.Details.Runs != nil {
   160  			runsCopy := make([]Run, len(v.Details.Runs))
   161  			copy(runsCopy, v.Details.Runs)
   162  			verdictsCopy[i].Details.Runs = runsCopy
   163  		}
   164  	}
   165  
   166  	return History{Verdicts: verdictsCopy}
   167  }
   168  
   169  // EvictBefore removes all verdicts prior (but not including) the given index.
   170  //
   171  // This will modify the verdicts buffer in-place, existing subslices
   172  // should be treated as invalid following this operation.
   173  func (h *History) EvictBefore(index int) {
   174  	// Instead of the obvious:
   175  	// h.Verdicts = h.Verdicts[index:]
   176  	// We shuffle all items forward by index so that we retain
   177  	// the same underlying Verdicts buffer, with the same capacity.
   178  
   179  	// Shuffle all items forward by 'index'.
   180  	for i := index; i < len(h.Verdicts); i++ {
   181  		h.Verdicts[i-index] = h.Verdicts[i]
   182  	}
   183  	h.Verdicts = h.Verdicts[:len(h.Verdicts)-index]
   184  }
   185  
   186  // Clear resets the input buffer to an empty state, similar to
   187  // its state after New().
   188  func (ib *Buffer) Clear() {
   189  	if cap(ib.HotBuffer.Verdicts) < ib.HotBufferCapacity+VerdictsInsertedHint {
   190  		// Indicates a logic error if someone discarded part of the
   191  		// originally allocated buffer.
   192  		panic("buffer capacity unexpectedly modified")
   193  	}
   194  	if cap(ib.ColdBuffer.Verdicts) < ib.ColdBufferCapacity+ib.HotBufferCapacity+VerdictsInsertedHint {
   195  		// Indicates a logic error if someone discarded part of the
   196  		// originally allocated buffer.
   197  		panic("buffer capacity unexpectedly modified")
   198  	}
   199  	ib.HotBuffer.Verdicts = ib.HotBuffer.Verdicts[:0]
   200  	ib.ColdBuffer.Verdicts = ib.ColdBuffer.Verdicts[:0]
   201  	ib.IsColdBufferDirty = false
   202  }
   203  
   204  // InsertVerdict inserts a new verdict into the input buffer.
   205  // It will first try to insert in the hot buffer, and if the hot buffer is full
   206  // as the result of the insert, then a compaction will occur.
   207  // If a compaction occurs, the IsColdBufferDirty flag will be set to true,
   208  // implying that the cold buffer content needs to be written to Spanner.
   209  func (ib *Buffer) InsertVerdict(v PositionVerdict) {
   210  	// Find the position to insert the verdict.
   211  	// As the new verdict is likely to have the latest commit position, we
   212  	// will iterate backwards from the end of the slice.
   213  	verdicts := ib.HotBuffer.Verdicts
   214  	pos := len(verdicts)
   215  	for ; pos > 0; pos-- {
   216  		if compareVerdict(v, verdicts[pos-1]) == 1 {
   217  			// verdict is after the verdict at position-1,
   218  			// so insert at position.
   219  			break
   220  		}
   221  	}
   222  
   223  	// Shuffle all verdicts in verdicts[pos:] forwards
   224  	// to create a spot at the insertion position.
   225  	// (We want to avoid allocating a new slice.)
   226  	verdicts = append(verdicts, PositionVerdict{})
   227  	for i := len(verdicts) - 1; i > pos; i-- {
   228  		verdicts[i] = verdicts[i-1]
   229  	}
   230  	verdicts[pos] = v
   231  	ib.HotBuffer.Verdicts = verdicts
   232  
   233  	if len(verdicts) == ib.HotBufferCapacity {
   234  		ib.IsColdBufferDirty = true
   235  		ib.Compact()
   236  	}
   237  }
   238  
   239  // Compact moves the content from the hot buffer to the cold buffer.
   240  // Note: It is possible that the cold buffer overflows after the compaction,
   241  // i.e., len(ColdBuffer.Verdicts) > ColdBufferCapacity.
   242  // This needs to be handled separately.
   243  func (ib *Buffer) Compact() {
   244  	var merged []PositionVerdict
   245  	ib.MergeBuffer(&merged)
   246  	ib.HotBuffer.Verdicts = ib.HotBuffer.Verdicts[:0]
   247  
   248  	// Copy the merged verdicts to the ColdBuffer instead of assigning
   249  	// the merged buffer, so that we keep the same pre-allocated buffer.
   250  	ib.ColdBuffer.Verdicts = ib.ColdBuffer.Verdicts[:0]
   251  	for _, v := range merged {
   252  		ib.ColdBuffer.Verdicts = append(ib.ColdBuffer.Verdicts, v)
   253  	}
   254  }
   255  
   256  // MergeBuffer merges the verdicts of the hot buffer and the cold buffer
   257  // into the provided slice, resizing it if necessary.
   258  // The returned slice will be sorted by commit position (oldest first), and
   259  // then by result time (oldest first).
   260  func (ib *Buffer) MergeBuffer(destination *[]PositionVerdict) {
   261  	// Because the hot buffer and cold buffer are both sorted, we can simply use
   262  	// a single merge to merge the 2 buffers.
   263  	hVerdicts := ib.HotBuffer.Verdicts
   264  	cVerdicts := ib.ColdBuffer.Verdicts
   265  
   266  	if *destination == nil {
   267  		*destination = make([]PositionVerdict, 0, ib.ColdBufferCapacity+ib.HotBufferCapacity+VerdictsInsertedHint)
   268  	}
   269  
   270  	// Reset destination slice to zero length.
   271  	merged := (*destination)[:0]
   272  
   273  	hPos := 0
   274  	cPos := 0
   275  	for hPos < len(hVerdicts) && cPos < len(cVerdicts) {
   276  		cmp := compareVerdict(hVerdicts[hPos], cVerdicts[cPos])
   277  		// Item in hot buffer is strictly older.
   278  		if cmp == -1 {
   279  			merged = append(merged, hVerdicts[hPos])
   280  			hPos++
   281  		} else {
   282  			merged = append(merged, cVerdicts[cPos])
   283  			cPos++
   284  		}
   285  	}
   286  
   287  	// Add the remaining items.
   288  	for ; hPos < len(hVerdicts); hPos++ {
   289  		merged = append(merged, hVerdicts[hPos])
   290  	}
   291  	for ; cPos < len(cVerdicts); cPos++ {
   292  		merged = append(merged, cVerdicts[cPos])
   293  	}
   294  
   295  	*destination = merged
   296  }
   297  
   298  // EvictionRange returns the part that should be evicted from cold buffer, due
   299  // to overflow.
   300  // Note: we never evict from the hot buffer due to overflow. Overflow from the
   301  // hot buffer should cause compaction to the cold buffer instead.
   302  // Returns:
   303  // - a boolean (shouldEvict) to indicated if an eviction should occur.
   304  // - a number (endIndex) for the eviction. The eviction will occur for range
   305  // [0, endIndex (inclusively)].
   306  // Note that eviction can only occur after a compaction from hot buffer to cold
   307  // buffer. It means the hot buffer is empty, and the cold buffer overflows.
   308  func (ib *Buffer) EvictionRange() (shouldEvict bool, endIndex int) {
   309  	if len(ib.ColdBuffer.Verdicts) <= ib.ColdBufferCapacity {
   310  		return false, 0
   311  	}
   312  	if len(ib.HotBuffer.Verdicts) > 0 {
   313  		panic("hot buffer is not empty during eviction")
   314  	}
   315  	return true, len(ib.ColdBuffer.Verdicts) - ib.ColdBufferCapacity - 1
   316  }
   317  
   318  func (ib *Buffer) Size() int {
   319  	return len(ib.ColdBuffer.Verdicts) + len(ib.HotBuffer.Verdicts)
   320  }
   321  
   322  // HistorySerializer provides methods to decode and encode History objects.
   323  // Methods on a given instance are only safe to call on one goroutine at
   324  // a time.
   325  type HistorySerializer struct {
   326  	// A preallocated buffer to store encoded, uncompressed verdicts.
   327  	// Avoids needing to allocate a new buffer for every decode/encode
   328  	// operation, with consequent heap requirements and GC churn.
   329  	tempBuf []byte
   330  }
   331  
   332  // ensureAndClearBuf returns a temporary buffer with suitable capacity
   333  // and zero length.
   334  func (hs *HistorySerializer) ensureAndClearBuf() {
   335  	if hs.tempBuf == nil {
   336  		// At most the history will have 2000 verdicts (cold buffer).
   337  		// Most verdicts will be simple expected verdict, so 30,000 is probably fine
   338  		// for most cases.
   339  		// In case 30,000 bytes is not enough, Encode() or Decode()
   340  		// will resize to an appropriate size.
   341  		hs.tempBuf = make([]byte, 0, 30000)
   342  	} else {
   343  		hs.tempBuf = hs.tempBuf[:0]
   344  	}
   345  }
   346  
   347  // Encode uses varint encoding to encode history into a byte array.
   348  // See go/luci-test-variant-analysis-design for details.
   349  func (hs *HistorySerializer) Encode(history History) []byte {
   350  	hs.ensureAndClearBuf()
   351  	buf := hs.tempBuf
   352  	buf = binary.AppendUvarint(buf, uint64(EncodingVersion))
   353  	buf = binary.AppendUvarint(buf, uint64(len(history.Verdicts)))
   354  
   355  	var lastPosition uint64
   356  	var lastHourNumber int64
   357  	for _, verdict := range history.Verdicts {
   358  		// We encode the relative deltaPosition between the current verdict and the
   359  		// previous verdicts.
   360  		deltaPosition := uint64(verdict.CommitPosition) - lastPosition
   361  		deltaPosition = deltaPosition << 1
   362  		if !verdict.IsSimpleExpectedPass {
   363  			// Set the last bit to 1 if it is not a simple verdict.
   364  			deltaPosition |= 1
   365  		}
   366  		buf = binary.AppendUvarint(buf, deltaPosition)
   367  		lastPosition = uint64(verdict.CommitPosition)
   368  
   369  		// Encode the "relative" hour.
   370  		// Note that the relative hour may be positive or negative. So we are encoding
   371  		// it as varint.
   372  		hourNumber := verdict.Hour.Unix() / 3600
   373  		deltaHour := hourNumber - lastHourNumber
   374  		buf = binary.AppendVarint(buf, deltaHour)
   375  		lastHourNumber = hourNumber
   376  
   377  		// Encode the verdict details, only if not simple verdict.
   378  		if !verdict.IsSimpleExpectedPass {
   379  			buf = appendVerdictDetails(buf, verdict.Details)
   380  		}
   381  	}
   382  	// It is possible the size of buf was increased in this method.
   383  	// If so, keep that larger buf for future encodings.
   384  	hs.tempBuf = buf
   385  
   386  	// Use zstd to compress the result. Note that the buffer returned
   387  	// by Compress is always different to hs.tempBuf, so tempBuf does
   388  	// not escape.
   389  	return span.Compress(buf)
   390  }
   391  
   392  // DecodeInto decodes the verdicts in buf, populating the history object.
   393  func (hs *HistorySerializer) DecodeInto(history *History, buf []byte) error {
   394  	// Clear existing verdicts to avoid state from a previous
   395  	// decoding leaking.
   396  	verdicts := history.Verdicts[:0]
   397  
   398  	var err error
   399  	hs.ensureAndClearBuf()
   400  	// If it is possible hs.tempBuf was resized to be able to accept
   401  	// all the decompressed content. If so, keep it, so we can use
   402  	// the larger buf for future decodings.
   403  	hs.tempBuf, err = span.Decompress(buf, hs.tempBuf)
   404  	if err != nil {
   405  		return errors.Annotate(err, "decompress error").Err()
   406  	}
   407  	reader := bytes.NewReader(hs.tempBuf)
   408  
   409  	// Read version.
   410  	version, err := binary.ReadUvarint(reader)
   411  	if err != nil {
   412  		return errors.Annotate(err, "read version").Err()
   413  	}
   414  	if version != EncodingVersion {
   415  		return fmt.Errorf("version mismatched: got version %d, want %d", version, EncodingVersion)
   416  	}
   417  
   418  	// Read verdicts.
   419  	nVerdicts, err := binary.ReadUvarint(reader)
   420  	if err != nil {
   421  		return errors.Annotate(err, "read number of verdicts").Err()
   422  	}
   423  	if nVerdicts > uint64(cap(verdicts)) {
   424  		// The caller has allocated an inappropriately sized buffer.
   425  		return errors.Reason("found %v verdicts to decode, but capacity is only %v", nVerdicts, cap(verdicts)).Err()
   426  	}
   427  
   428  	for i := 0; i < int(nVerdicts); i++ {
   429  		// Get the commit position for the verdicts, and if the verdict is simple
   430  		// expected.
   431  		verdict := PositionVerdict{}
   432  		posSim, err := binary.ReadUvarint(reader)
   433  		if err != nil {
   434  			return errors.Annotate(err, "read position simple verdict").Err()
   435  		}
   436  		deltaPos, isSimple := decodePositionSimpleVerdict(posSim)
   437  
   438  		verdict.IsSimpleExpectedPass = isSimple
   439  		// First verdict, deltaPos should be the absolute commit position.
   440  		if i == 0 {
   441  			verdict.CommitPosition = deltaPos
   442  		} else {
   443  			// deltaPos records the relative difference.
   444  			verdict.CommitPosition = verdicts[i-1].CommitPosition + deltaPos
   445  		}
   446  
   447  		// Get the hour.
   448  		deltaHour, err := binary.ReadVarint(reader)
   449  		if err != nil {
   450  			return errors.Annotate(err, "read delta hour").Err()
   451  		}
   452  		if i == 0 {
   453  			verdict.Hour = time.Unix(deltaHour*3600, 0)
   454  		} else {
   455  			secs := verdicts[i-1].Hour.Unix()
   456  			verdict.Hour = time.Unix(secs+deltaHour*3600, 0)
   457  		}
   458  
   459  		// Read the verdict details.
   460  		if !isSimple {
   461  			vd, err := readVerdictDetails(reader)
   462  			if err != nil {
   463  				return errors.Annotate(err, "read verdict details").Err()
   464  			}
   465  			verdict.Details = vd
   466  		}
   467  		verdicts = append(verdicts, verdict)
   468  	}
   469  	history.Verdicts = verdicts
   470  
   471  	return err
   472  }
   473  
   474  func readVerdictDetails(reader *bytes.Reader) (VerdictDetails, error) {
   475  	vd := VerdictDetails{}
   476  	// Get IsExonerated.
   477  	exoInt, err := binary.ReadUvarint(reader)
   478  	if err != nil {
   479  		return vd, errors.Annotate(err, "read exoneration status").Err()
   480  	}
   481  	vd.IsExonerated = uInt64ToBool(exoInt)
   482  
   483  	// Get runs.
   484  	runCount, err := binary.ReadUvarint(reader)
   485  	if err != nil {
   486  		return vd, errors.Annotate(err, "read run count").Err()
   487  	}
   488  	vd.Runs = make([]Run, runCount)
   489  	for i := 0; i < int(runCount); i++ {
   490  		run, err := readRun(reader)
   491  		if err != nil {
   492  			return vd, errors.Annotate(err, "read run").Err()
   493  		}
   494  		vd.Runs[i] = run
   495  	}
   496  	return vd, nil
   497  }
   498  
   499  func readRun(reader *bytes.Reader) (Run, error) {
   500  	r := Run{}
   501  	// Read expected passed count.
   502  	expectedPassedCount, err := binary.ReadUvarint(reader)
   503  	if err != nil {
   504  		return r, errors.Annotate(err, "read expected passed count").Err()
   505  	}
   506  	r.Expected.PassCount = int(expectedPassedCount)
   507  
   508  	// Read expected failed count.
   509  	expectedFailedCount, err := binary.ReadUvarint(reader)
   510  	if err != nil {
   511  		return r, errors.Annotate(err, "read expected failed count").Err()
   512  	}
   513  	r.Expected.FailCount = int(expectedFailedCount)
   514  
   515  	// Read expected crashed count.
   516  	expectedCrashedCount, err := binary.ReadUvarint(reader)
   517  	if err != nil {
   518  		return r, errors.Annotate(err, "read expected crashed count").Err()
   519  	}
   520  	r.Expected.CrashCount = int(expectedCrashedCount)
   521  
   522  	// Read expected aborted count.
   523  	expectedAbortedCount, err := binary.ReadUvarint(reader)
   524  	if err != nil {
   525  		return r, errors.Annotate(err, "read expected aborted count").Err()
   526  	}
   527  	r.Expected.AbortCount = int(expectedAbortedCount)
   528  
   529  	// Read unexpected passed count.
   530  	unexpectedPassedCount, err := binary.ReadUvarint(reader)
   531  	if err != nil {
   532  		return r, errors.Annotate(err, "read unexpected passed count").Err()
   533  	}
   534  	r.Unexpected.PassCount = int(unexpectedPassedCount)
   535  
   536  	// Read unexpected failed count.
   537  	unexpectedFailedCount, err := binary.ReadUvarint(reader)
   538  	if err != nil {
   539  		return r, errors.Annotate(err, "read unexpected failed count").Err()
   540  	}
   541  	r.Unexpected.FailCount = int(unexpectedFailedCount)
   542  
   543  	// Read unexpected crashed count.
   544  	unexpectedCrashedCount, err := binary.ReadUvarint(reader)
   545  	if err != nil {
   546  		return r, errors.Annotate(err, "read unexpected crashed count").Err()
   547  	}
   548  	r.Unexpected.CrashCount = int(unexpectedCrashedCount)
   549  
   550  	// Read unexpected aborted count.
   551  	unexpectedAbortedCount, err := binary.ReadUvarint(reader)
   552  	if err != nil {
   553  		return r, errors.Annotate(err, "read unexpected aborted count").Err()
   554  	}
   555  	r.Unexpected.AbortCount = int(unexpectedAbortedCount)
   556  
   557  	// Read isDuplicate
   558  	isDuplicate, err := binary.ReadUvarint(reader)
   559  	if err != nil {
   560  		return r, errors.Annotate(err, "read is duplicate").Err()
   561  	}
   562  	r.IsDuplicate = uInt64ToBool(isDuplicate)
   563  
   564  	return r, nil
   565  }
   566  
   567  func appendVerdictDetails(result []byte, vd VerdictDetails) []byte {
   568  	// Encode IsExonerated.
   569  	result = binary.AppendUvarint(result, boolToUInt64(vd.IsExonerated))
   570  
   571  	// Encode runs.
   572  	result = binary.AppendUvarint(result, uint64(len(vd.Runs)))
   573  	for _, r := range vd.Runs {
   574  		result = appendRun(result, r)
   575  	}
   576  	return result
   577  }
   578  
   579  func appendRun(result []byte, run Run) []byte {
   580  	result = binary.AppendUvarint(result, uint64(run.Expected.PassCount))
   581  	result = binary.AppendUvarint(result, uint64(run.Expected.FailCount))
   582  	result = binary.AppendUvarint(result, uint64(run.Expected.CrashCount))
   583  	result = binary.AppendUvarint(result, uint64(run.Expected.AbortCount))
   584  	result = binary.AppendUvarint(result, uint64(run.Unexpected.PassCount))
   585  	result = binary.AppendUvarint(result, uint64(run.Unexpected.FailCount))
   586  	result = binary.AppendUvarint(result, uint64(run.Unexpected.CrashCount))
   587  	result = binary.AppendUvarint(result, uint64(run.Unexpected.AbortCount))
   588  	result = binary.AppendUvarint(result, boolToUInt64(run.IsDuplicate))
   589  	return result
   590  }
   591  
   592  // decodePositionSimpleVerdict decodes the value posSim and returns 2 values.
   593  // 1. The (delta) commit position.
   594  // 2. Whether the verdict is a simple expected passed verdict.
   595  // The last bit of posSim is set to 1 if the verdict is NOT a simple expected pass.
   596  func decodePositionSimpleVerdict(posSim uint64) (int, bool) {
   597  	isSimple := false
   598  	lastBit := posSim & 1
   599  	if lastBit == 0 {
   600  		isSimple = true
   601  	}
   602  	deltaPos := posSim >> 1
   603  	return int(deltaPos), isSimple
   604  }
   605  
   606  func boolToUInt64(b bool) uint64 {
   607  	result := 0
   608  	if b {
   609  		result = 1
   610  	}
   611  	return uint64(result)
   612  }
   613  
   614  func uInt64ToBool(u uint64) bool {
   615  	return u == 1
   616  }
   617  
   618  // compareVerdict returns 1 if v1 is later than v2, -1 if v1 is earlier than
   619  // v2, and 0 if they are at the same time.
   620  // The comparision is done on commit position, then on hour.
   621  func compareVerdict(v1 PositionVerdict, v2 PositionVerdict) int {
   622  	if v1.CommitPosition > v2.CommitPosition {
   623  		return 1
   624  	}
   625  	if v1.CommitPosition < v2.CommitPosition {
   626  		return -1
   627  	}
   628  	if v1.Hour.Unix() > v2.Hour.Unix() {
   629  		return 1
   630  	}
   631  	if v1.Hour.Unix() < v2.Hour.Unix() {
   632  		return -1
   633  	}
   634  	return 0
   635  }