github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/web/src/LogStore.ts (about)

     1  // Client-side log store, which helps client side rendering and filtering of logs.
     2  //
     3  // Loosely adapted from the data structures in
     4  // pkg/model/logstore/logstore.go
     5  // but with better support for incremental updates and rendering.
     6  
     7  import React, { useContext } from "react"
     8  import { isBuildSpanId } from "./logs"
     9  import { LogLevel, LogLine, LogPatchSet } from "./types"
    10  
    11  // Firestore doesn't properly handle maps with keys equal to the empty string, so
    12  // we normalize all empty span ids to '_' client-side.
    13  const defaultSpanId = "_"
    14  const fieldNameProgressId = "progressID"
    15  
    16  const defaultMaxLogLength = 2 * 1000 * 1000
    17  
    18  // Index all warnings and errors by span.
    19  export type LogAlert = {
    20    lineIndex: number
    21    level: LogLevel
    22  }
    23  
    24  // LogStore implements LogAlertIndex, a narrower interface for fetching
    25  // the alerts for a particular span.
    26  //
    27  // Consumers of LogAlertIndex shouldn't assume it's a LogStore. In the future,
    28  // we may break them up into separate objects.
    29  export interface LogAlertIndex {
    30    alertsForSpanId(spanId: string): LogAlert[]
    31  }
    32  
    33  type LogSpan = {
    34    spanId: string
    35    manifestName: string
    36    firstLineIndex: number
    37    lastLineIndex: number
    38    alerts: LogAlert[]
    39  }
    40  
    41  type LogWarning = {
    42    anchorIndex: number
    43    spanId: string
    44    text: string
    45  }
    46  
    47  class StoredLine {
    48    spanId: string
    49    time: string
    50    text: string
    51    level: string
    52    anchor: boolean
    53    fields: { [key: string]: string } | null
    54  
    55    constructor(seg: Proto.webviewLogSegment) {
    56      this.spanId = seg.spanId || defaultSpanId
    57      this.time = seg.time ?? ""
    58      this.text = seg.text ?? ""
    59      this.level = seg.level ?? "INFO"
    60      this.anchor = seg.anchor ?? false
    61      this.fields = (seg.fields as { [key: string]: string }) ?? null
    62    }
    63  
    64    field(key: string) {
    65      if (!this.fields) {
    66        return ""
    67      }
    68      return this.fields[key] ?? ""
    69    }
    70  
    71    isComplete() {
    72      return this.text[this.text.length - 1] === "\n"
    73    }
    74  
    75    canContinueLine(other: StoredLine) {
    76      return this.level === other.level && this.spanId === other.spanId
    77    }
    78  }
    79  
    80  export enum LogUpdateAction {
    81    append,
    82    truncate,
    83  }
    84  
    85  export interface LogUpdateEvent {
    86    action: LogUpdateAction
    87  }
    88  
    89  type callback = (e: LogUpdateEvent) => void
    90  
    91  class LogStore implements LogAlertIndex {
    92    // Track which segments we've received from the server.
    93    checkpoint: number
    94  
    95    spans: { [key: string]: LogSpan }
    96  
    97    // These are held in-memory so we can send them on snapshot, and are
    98    // also used to help with incremental log rendering.
    99    segments: Proto.webviewLogSegment[]
   100  
   101    // A map of segment indices to the line indices that they rendered.
   102    segmentToLine: number[]
   103  
   104    // As segments are appended, we fold them into our internal line-by-line model
   105    // for rendering.
   106    lines: StoredLine[]
   107  
   108    // A cache of the react data model
   109    lineCache: { [key: number]: LogLine }
   110  
   111    updateCallbacks: callback[]
   112  
   113    // Track log length, for truncation.
   114    logLength: number = 0
   115    maxLogLength: number
   116  
   117    constructor() {
   118      this.spans = {}
   119      this.segments = []
   120      this.segmentToLine = []
   121      this.lines = []
   122      this.checkpoint = 0
   123      this.lineCache = {}
   124      this.updateCallbacks = []
   125      this.maxLogLength = defaultMaxLogLength
   126    }
   127  
   128    addUpdateListener(c: callback) {
   129      if (!this.updateCallbacks.includes(c)) {
   130        this.updateCallbacks.push(c)
   131      }
   132    }
   133  
   134    removeUpdateListener(c: callback) {
   135      this.updateCallbacks = this.updateCallbacks.filter((item) => item !== c)
   136    }
   137  
   138    hasLinesForSpan(spanId: string): boolean {
   139      const span = this.spans[spanId]
   140      return span && span.firstLineIndex !== -1
   141    }
   142  
   143    toLogList(maxSize: number | null | undefined): Proto.webviewLogList {
   144      let spans = {} as { [key: string]: Proto.webviewLogSpan }
   145  
   146      let size = 0
   147      const segments = [] as Proto.webviewLogSegment[]
   148      for (let i = this.segments.length - 1; i >= 0; i--) {
   149        let segment = this.segments[i]
   150        size += segment.text?.length || 0
   151        if (maxSize && size > maxSize) {
   152          break
   153        }
   154  
   155        let spanId = segment.spanId
   156        if (spanId && !spans[spanId]) {
   157          spans[spanId] = { manifestName: this.spans[spanId].manifestName }
   158        }
   159  
   160        segments.push({
   161          spanId: spanId,
   162          time: segment.time,
   163          text: segment.text,
   164          level: segment.level,
   165          fields: segment.fields,
   166        })
   167      }
   168  
   169      // caller expects segments in chronological order
   170      // (iteration here was done backwards for truncation)
   171      segments.reverse()
   172  
   173      return {
   174        spans: spans,
   175        segments: segments,
   176      }
   177    }
   178  
   179    append(logList: Proto.webviewLogList) {
   180      let newSpans = logList.spans as { [key: string]: Proto.webviewLogSpan }
   181      let newSegments = logList.segments ?? []
   182      let fromCheckpoint = logList.fromCheckpoint ?? 0
   183      let toCheckpoint = logList.toCheckpoint ?? 0
   184      if (fromCheckpoint < 0) {
   185        return
   186      }
   187  
   188      if (fromCheckpoint < this.checkpoint) {
   189        // The server is re-sending some logs we already have, so slice them off.
   190        let deleteCount = this.checkpoint - fromCheckpoint
   191        newSegments = newSegments.slice(deleteCount)
   192      }
   193  
   194      if (toCheckpoint > this.checkpoint) {
   195        this.checkpoint = toCheckpoint
   196      }
   197  
   198      for (let key in newSpans) {
   199        let spanId = key || defaultSpanId
   200        let existingSpan = this.spans[spanId]
   201        if (!existingSpan) {
   202          this.spans[spanId] = {
   203            spanId: spanId,
   204            manifestName: newSpans[key].manifestName ?? "",
   205            firstLineIndex: -1,
   206            lastLineIndex: -1,
   207            alerts: [],
   208          }
   209        }
   210      }
   211  
   212      newSegments.forEach((segment) => this.addSegment(segment))
   213  
   214      this.invokeUpdateCallbacks({
   215        action: LogUpdateAction.append,
   216      })
   217  
   218      this.ensureMaxLength()
   219    }
   220  
   221    // Returns a list of all error and warning log lines in this span,
   222    // and their line index. Consumers must not mutate the list.
   223    alertsForSpanId(spanId: string): LogAlert[] {
   224      let span = this.spans[spanId]
   225      if (!span) {
   226        return []
   227      }
   228      return span.alerts
   229    }
   230  
   231    private invokeUpdateCallbacks(e: LogUpdateEvent) {
   232      window.requestAnimationFrame(() => {
   233        // Make sure an exception in one callback doesn't affect the rest.
   234        try {
   235          this.updateCallbacks.forEach((c) => c(e))
   236        } catch (e) {
   237          window.requestAnimationFrame(() => {
   238            throw e
   239          })
   240        }
   241      })
   242    }
   243  
   244    private addSegment(newSegment: Proto.webviewLogSegment) {
   245      // workaround firestore bug. see comments on defaultSpanId.
   246      newSegment.spanId = newSegment.spanId || defaultSpanId
   247      this.segments.push(newSegment)
   248      this.logLength += newSegment.text?.length || 0
   249  
   250      let candidate = new StoredLine(newSegment)
   251      let spanId = candidate.spanId
   252      let span = this.spans[spanId]
   253      if (!span) {
   254        // If we don't have the span for this log, we can't meaningfully print it,
   255        // so just drop it. This means that there's a bug on the server, and
   256        // the best the client can do is fail gracefully.
   257        this.segmentToLine.push(-1)
   258        return
   259      }
   260      let isStartingNewLine = false
   261      if (span.lastLineIndex === -1) {
   262        isStartingNewLine = true
   263        this.segmentToLine.push(this.lines.length)
   264      } else {
   265        let line = this.lines[span.lastLineIndex]
   266        let overwriteIndex = this.maybeOverwriteLine(candidate, span)
   267        if (overwriteIndex !== -1) {
   268          this.segmentToLine.push(overwriteIndex)
   269          return
   270        } else if (line.isComplete() || !line.canContinueLine(candidate)) {
   271          isStartingNewLine = true
   272          this.segmentToLine.push(this.lines.length)
   273        } else {
   274          line.text += candidate.text
   275          delete this.lineCache[span.lastLineIndex]
   276          this.segmentToLine.push(span.lastLineIndex)
   277          return
   278        }
   279      }
   280  
   281      if (span.firstLineIndex === -1) {
   282        span.firstLineIndex = this.lines.length
   283      }
   284  
   285      if (isStartingNewLine) {
   286        let lineIndex = this.lines.length
   287        span.lastLineIndex = lineIndex
   288        this.lines.push(candidate)
   289  
   290        // If this starts a warning or error, index it now.
   291        let level = newSegment.level
   292        if (
   293          newSegment.anchor &&
   294          (level === LogLevel.WARN || level === LogLevel.ERROR)
   295        ) {
   296          span.alerts.push({ level, lineIndex })
   297        }
   298      }
   299    }
   300  
   301    // Remove spans from the LogStore, triggering a full rebuild of the line cache.
   302    removeSpans(spanIds: string[]) {
   303      if (spanIds.length === 0) {
   304        return
   305      }
   306  
   307      this.logLength = 0
   308      this.lines = []
   309      this.lineCache = []
   310      this.segmentToLine = []
   311      const spansToDelete = new Set(spanIds)
   312      if (spansToDelete.has("")) {
   313        spansToDelete.delete("")
   314        spansToDelete.add(defaultSpanId)
   315      }
   316  
   317      for (const span of Object.values(this.spans)) {
   318        const spanId = span.spanId
   319        if (spansToDelete.has(spanId)) {
   320          delete this.spans[spanId]
   321        } else {
   322          span.firstLineIndex = -1
   323          span.lastLineIndex = -1
   324          span.alerts = []
   325        }
   326      }
   327  
   328      const currentSegments = this.segments
   329      this.segments = []
   330      for (const segment of currentSegments) {
   331        const spanId = segment.spanId
   332        if (spanId && !spansToDelete.has(spanId)) {
   333          // re-add any non-deleted segments
   334          this.addSegment(segment)
   335        }
   336      }
   337  
   338      this.invokeUpdateCallbacks({
   339        action: LogUpdateAction.truncate,
   340      })
   341    }
   342  
   343    // If this line has a progress id, see if we can overwrite a previous line.
   344    // Return the index of the line we were able to overwrite, or -1 otherwise.
   345    private maybeOverwriteLine(candidate: StoredLine, span: LogSpan): number {
   346      let progressId = candidate.field(fieldNameProgressId)
   347      if (!progressId) {
   348        return -1
   349      }
   350  
   351      // Iterate backwards and figure out which line to overwrite.
   352      for (let i = span.lastLineIndex; i >= span.firstLineIndex; i--) {
   353        let cur = this.lines[i]
   354        if (cur.spanId !== candidate.spanId) {
   355          // skip lines from other spans
   356          // TODO(nick): maybe we should track if spans are interleaved, and rearrange the
   357          // lines to make more sense?
   358          continue
   359        }
   360  
   361        // If we're outside the "progress" zone, we couldn't find it.
   362        let curProgressId = cur.field(fieldNameProgressId)
   363        if (!curProgressId) {
   364          return -1
   365        }
   366  
   367        if (progressId !== curProgressId) {
   368          continue
   369        }
   370  
   371        cur.text = candidate.text
   372        delete this.lineCache[i]
   373        return i
   374      }
   375      return -1
   376    }
   377  
   378    allLog(): LogLine[] {
   379      return this.logHelper(this.spans, 0).lines
   380    }
   381  
   382    allLogPatchSet(checkpoint: number): LogPatchSet {
   383      return this.logHelper(this.spans, checkpoint)
   384    }
   385  
   386    spanLog(spanIds: string[]): LogLine[] {
   387      let spans: { [key: string]: LogSpan } = {}
   388      spanIds.forEach((spanId) => {
   389        spanId = spanId ? spanId : defaultSpanId
   390        let span = this.spans[spanId]
   391        if (span) {
   392          spans[spanId] = span
   393        }
   394      })
   395  
   396      return this.logHelper(spans, 0).lines
   397    }
   398  
   399    allSpans(): { [key: string]: LogSpan } {
   400      const result: { [key: string]: LogSpan } = {}
   401      for (let spanId in this.spans) {
   402        result[spanId] = this.spans[spanId]
   403      }
   404      return result
   405    }
   406  
   407    spansForManifest(mn: string): { [key: string]: LogSpan } {
   408      let result: { [key: string]: LogSpan } = {}
   409      for (let spanId in this.spans) {
   410        let span = this.spans[spanId]
   411        if (span.manifestName === mn) {
   412          result[spanId] = span
   413        }
   414      }
   415      return result
   416    }
   417  
   418    getOrderedBuildSpanIds(spanId: string): string[] {
   419      let startSpan = this.spans[spanId]
   420      if (!startSpan) {
   421        return []
   422      }
   423  
   424      let manifestName = startSpan.manifestName
   425      const spanIds: string[] = []
   426      for (let key in this.spans) {
   427        if (!isBuildSpanId(key)) {
   428          continue
   429        }
   430  
   431        let span = this.spans[key]
   432        if (span.manifestName !== manifestName) {
   433          continue
   434        }
   435  
   436        spanIds.push(key)
   437      }
   438  
   439      return this.sortedSpanIds(spanIds)
   440    }
   441  
   442    getOrderedBuildSpans(spanId: string): LogSpan[] {
   443      return this.getOrderedBuildSpanIds(spanId).map(
   444        (spanId) => this.spans[spanId]
   445      )
   446    }
   447  
   448    private sortedSpanIds(spanIds: string[]): string[] {
   449      return spanIds.sort((a, b) => {
   450        return this.spans[a].firstLineIndex - this.spans[b].firstLineIndex
   451      })
   452    }
   453  
   454    // Given a build span in the current manifest, find the next build span.
   455    nextBuildSpan(spanId: string): LogSpan | null {
   456      let spanIds = this.getOrderedBuildSpanIds(spanId)
   457      let currentIndex = spanIds.indexOf(spanId)
   458      if (currentIndex === -1 || currentIndex === spanIds.length - 1) {
   459        return null
   460      }
   461      return this.spans[spanIds[currentIndex + 1]]
   462    }
   463  
   464    // Find all the logs "caused" by a particular build.
   465    //
   466    // Eventually, we should add causality links between spans to the
   467    // data model itself! c.f., Links in open-tracing
   468    // https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/api-tracing.md#add-links
   469    // But for now, we just hack some spans together based on their manifest name
   470    // and when they showed up.
   471    traceLog(spanId: string): LogLine[] {
   472      // Currently, we only support tracing of build logs.
   473      if (!isBuildSpanId(spanId)) {
   474        return []
   475      }
   476  
   477      let startSpan = this.spans[spanId]
   478      let spans: { [key: string]: LogSpan } = {}
   479      spans[spanId] = startSpan
   480  
   481      let nextBuildSpan = this.nextBuildSpan(spanId)
   482  
   483      // Grab all the spans that start between this span and the next build.
   484      //
   485      // TODO(nick): This currently skips any events that happen
   486      // because they're part of an "events" span where the causality
   487      // is uncertain. We should be more intelligent about sucking in events.
   488      for (let key in this.spans) {
   489        let candidate = this.spans[key]
   490        if (candidate.manifestName !== startSpan.manifestName) {
   491          continue
   492        }
   493  
   494        if (
   495          candidate.firstLineIndex > startSpan.firstLineIndex &&
   496          (!nextBuildSpan ||
   497            candidate.firstLineIndex < nextBuildSpan.firstLineIndex)
   498        ) {
   499          spans[key] = candidate
   500        }
   501      }
   502  
   503      return this.logHelper(spans, 0).lines
   504    }
   505  
   506    manifestLog(mn: string): LogLine[] {
   507      let spans = this.spansForManifest(mn)
   508      return this.logHelper(spans, 0).lines
   509    }
   510  
   511    manifestLogPatchSet(mn: string, checkpoint: number): LogPatchSet {
   512      let spans = this.spansForManifest(mn)
   513      return this.logHelper(spans, checkpoint)
   514    }
   515  
   516    starredLogPatchSet(stars: string[], checkpoint: number): LogPatchSet {
   517      let result: { [key: string]: LogSpan } = {}
   518      for (let spanId in this.spans) {
   519        let span = this.spans[spanId]
   520        if (stars.includes(span.manifestName)) {
   521          result[spanId] = span
   522        }
   523      }
   524      return this.logHelper(result, checkpoint)
   525    }
   526  
   527    // Return all the logs for the given options.
   528    //
   529    // spansToLog: Filtering by an arbitrary set of spans.
   530    // checkpoint: Continuation from an earlier checkpoint, only returning lines updated
   531    //   since that checkpoint. Pass 0 to return all logs.
   532    logHelper(
   533      spansToLog: { [key: string]: LogSpan },
   534      checkpoint: number
   535    ): LogPatchSet {
   536      let result: LogLine[] = []
   537  
   538      // We want to print the log line-by-line, but we don't actually store the logs
   539      // line-by-line. We store them as segments.
   540      //
   541      // This means we need to:
   542      // 1) At segment x,
   543      // 2) If x starts a new line, print it, then run ahead to print the rest of the line
   544      //    until the entire line is consumed.
   545      // 3) If x does not start a new line, skip it, because we assume it was handled
   546      //    in a previous line.
   547      //
   548      // This can have some O(n^2) perf characteristics in the worst case, but
   549      // for normal inputs should be fine.
   550      let startIndex = 0
   551      let lastIndex = this.lines.length - 1
   552      let isFilteredLog =
   553        Object.keys(spansToLog).length !== Object.keys(this.spans).length
   554      if (isFilteredLog) {
   555        let earliestStartIndex = -1
   556        let latestEndIndex = -1
   557        for (let spanId in spansToLog) {
   558          let span = spansToLog[spanId]
   559          if (
   560            earliestStartIndex === -1 ||
   561            (span.firstLineIndex !== -1 &&
   562              span.firstLineIndex < earliestStartIndex)
   563          ) {
   564            earliestStartIndex = span.firstLineIndex
   565          }
   566          if (
   567            latestEndIndex === -1 ||
   568            (span.lastLineIndex !== -1 && span.lastLineIndex > latestEndIndex)
   569          ) {
   570            latestEndIndex = span.lastLineIndex
   571          }
   572        }
   573  
   574        if (earliestStartIndex === -1) {
   575          return { lines: [], checkpoint: checkpoint }
   576        }
   577  
   578        startIndex = earliestStartIndex
   579        lastIndex = latestEndIndex
   580      }
   581  
   582      // Only look at segments that have come in since the last checkpoint.
   583      let incremental = checkpoint > 0
   584      let linesToLog: { [key: number]: boolean } = {}
   585      if (incremental) {
   586        let earliestStartIndex = -1
   587        for (let i = checkpoint; i < this.segments.length; i++) {
   588          let segment = this.segments[i]
   589          let span = spansToLog[segment.spanId || defaultSpanId]
   590          if (!span) {
   591            continue
   592          }
   593  
   594          let lineIndex = this.segmentToLine[i]
   595          if (earliestStartIndex === -1 || lineIndex < earliestStartIndex) {
   596            earliestStartIndex = lineIndex
   597          }
   598          linesToLog[lineIndex] = true
   599        }
   600  
   601        if (earliestStartIndex !== -1 && earliestStartIndex > startIndex) {
   602          startIndex = earliestStartIndex
   603        }
   604      }
   605  
   606      for (let i = startIndex; i <= lastIndex; i++) {
   607        let storedLine = this.lines[i]
   608        let spanId = storedLine.spanId
   609        let span = spansToLog[spanId]
   610        if (!span) {
   611          continue
   612        }
   613  
   614        if (incremental && !linesToLog[i]) {
   615          continue
   616        }
   617  
   618        let line = this.lineCache[i]
   619        if (!line) {
   620          let text = storedLine.text
   621          // strip off the newline
   622          if (text[text.length - 1] === "\n") {
   623            text = text.substring(0, text.length - 1)
   624          }
   625          line = {
   626            text: text,
   627            level: storedLine.level,
   628            manifestName: span.manifestName,
   629            buildEvent: storedLine.fields?.buildEvent,
   630            spanId: spanId,
   631            storedLineIndex: i,
   632          }
   633  
   634          this.lineCache[i] = line
   635        }
   636  
   637        result.push(line)
   638      }
   639  
   640      return {
   641        lines: result,
   642        checkpoint: this.segments.length,
   643      }
   644    }
   645  
   646    // After a log hits its limit, we need to truncate it to keep it small
   647    // we do this by cutting a big chunk at a time, so that we have rarer, larger changes, instead of
   648    // a small change every time new data is written to the log
   649    // https://github.com/tilt-dev/tilt/issues/1935#issuecomment-531390353
   650    logTruncationTarget(): number {
   651      return this.maxLogLength / 2
   652    }
   653  
   654    ensureMaxLength() {
   655      if (this.logLength <= this.maxLogLength) {
   656        return
   657      }
   658  
   659      // First, count the number of bytes in each manifest.
   660      let manifestWeights: {
   661        [key: string]: { name: string; byteCount: number; start: string }
   662      } = {}
   663  
   664      for (let segment of this.segments) {
   665        let span = this.spans[segment.spanId || defaultSpanId]
   666        if (span) {
   667          let name = span.manifestName || ""
   668          let weight = manifestWeights[name]
   669          if (!weight) {
   670            weight = { name, byteCount: 0, start: segment.time || "" }
   671            manifestWeights[name] = weight
   672          }
   673          weight.byteCount += segment.text?.length || 0
   674        }
   675      }
   676  
   677      // Next, repeatedly cut the longest manifest in half until
   678      // we've reached the target number of bytes to cut.
   679      let leftToCut = this.logLength - this.logTruncationTarget()
   680      while (leftToCut > 0) {
   681        let mn = this.heaviestManifestName(manifestWeights)
   682        let amountToCut = Math.ceil(manifestWeights[mn].byteCount / 2)
   683        if (amountToCut > leftToCut) {
   684          amountToCut = leftToCut
   685        }
   686        leftToCut -= amountToCut
   687        manifestWeights[mn].byteCount -= amountToCut
   688      }
   689  
   690      // Lastly, go through all the segments, and truncate the manifests
   691      // where we said we would.
   692      let newSegments = []
   693      let trimmedSegmentCount = 0
   694      for (let i = this.segments.length - 1; i >= 0; i--) {
   695        let segment = this.segments[i]
   696        let span = this.spans[segment.spanId || defaultSpanId]
   697        let mn = span?.manifestName || ""
   698        let len = segment.text?.length || 0
   699        manifestWeights[mn].byteCount -= len
   700        if (manifestWeights[mn].byteCount < 0) {
   701          trimmedSegmentCount++
   702          continue
   703        }
   704  
   705        newSegments.push(segment)
   706      }
   707  
   708      newSegments.reverse()
   709  
   710      // Reset the state of the logstore.
   711      this.logLength = 0
   712      this.lines = []
   713      this.lineCache = []
   714      this.segmentToLine = []
   715  
   716      for (const span of Object.values(this.spans)) {
   717        span.firstLineIndex = -1
   718        span.lastLineIndex = -1
   719        span.alerts = []
   720      }
   721  
   722      this.segments = []
   723      for (const segment of newSegments) {
   724        this.addSegment(segment)
   725      }
   726  
   727      this.invokeUpdateCallbacks({
   728        action: LogUpdateAction.truncate,
   729      })
   730    }
   731  
   732    // There are 3 types of logs we need to consider:
   733    // 1) Jobs that print short, critical information at the start.
   734    // 2) Jobs that print lots of health checks continuously.
   735    // 3) Jobs that print recent test results.
   736    //
   737    // Truncating purely on recency would be bad for (1).
   738    // Truncating purely on length would be bad for (3).
   739    //
   740    // So we weight based on both recency and length.
   741    heaviestManifestName(manifestWeights: {
   742      [key: string]: { name: string; byteCount: number; start: string }
   743    }): string {
   744      // Sort manifests by most recent first.
   745      let manifestsByTime = Object.values(manifestWeights).sort((a, b) => {
   746        if (a.start != b.start) {
   747          return a.start < b.start ? 1 : -1
   748        }
   749        if (a.name != b.name) {
   750          return a.name < b.name ? 1 : -1
   751        }
   752        return 0
   753      })
   754  
   755      let heaviest = ""
   756      let heaviestValue = -1
   757      for (let i = 0; i < manifestsByTime.length; i++) {
   758        // We compute: weightValue = order * byteCount where the manifest with
   759        // most recent logs has order 1, the next one has order 2, and so on.
   760        //
   761        // This helps ensures older logs get truncated first.
   762        let order = i + 1
   763        let value = order * manifestsByTime[i].byteCount
   764        if (value > heaviestValue) {
   765          heaviest = manifestsByTime[i].name
   766          heaviestValue = value
   767        }
   768      }
   769      return heaviest
   770    }
   771  }
   772  
   773  export default LogStore
   774  
   775  const logStoreContext = React.createContext<LogStore>(new LogStore())
   776  
   777  export function useLogStore(): LogStore {
   778    return useContext(logStoreContext)
   779  }
   780  
   781  // LogAlertIndex provides access to warnings/errors without the rest of the
   782  // LogStore.
   783  //
   784  // Consumers of LogAlertIndex shouldn't assume it's a LogStore. In the future,
   785  // we may break them up into separate objects.
   786  export function useLogAlertIndex(): LogAlertIndex {
   787    return useContext(logStoreContext)
   788  }
   789  
   790  export let LogStoreProvider = logStoreContext.Provider