Make TimeSeries an interface.

Since it's being used outside of the profiler package. Also make the canonical way to create a TimeSeries to be Profiler.instance().createTimeSeries(...).

PiperOrigin-RevId: 828069345
Change-Id: I086e0ead2c8324fec1d72fae2c8f73b2890789c7
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/BUILD b/src/main/java/com/google/devtools/build/lib/profiler/BUILD
index f2bed4f..782c05f 100644
--- a/src/main/java/com/google/devtools/build/lib/profiler/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/profiler/BUILD
@@ -56,6 +56,7 @@
         "StatRecorder.java",
         "ThreadMetadata.java",
         "TimeSeries.java",
+        "TimeSeriesImpl.java",
         "TraceData.java",
     ],
     deps = [
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/CollectLocalResourceUsage.java b/src/main/java/com/google/devtools/build/lib/profiler/CollectLocalResourceUsage.java
index 3d7fbcf..ed357b5 100644
--- a/src/main/java/com/google/devtools/build/lib/profiler/CollectLocalResourceUsage.java
+++ b/src/main/java/com/google/devtools/build/lib/profiler/CollectLocalResourceUsage.java
@@ -260,7 +260,8 @@
         return;
       }
       var series =
-          timeSeries.computeIfAbsent(type, unused -> new TimeSeries(startTime, BUCKET_DURATION));
+          timeSeries.computeIfAbsent(
+              type, unused -> Profiler.instance().createTimeSeries(startTime, BUCKET_DURATION));
       series.addRange(previousElapsed, nextElapsed, value);
     }
   }
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/Profiler.java b/src/main/java/com/google/devtools/build/lib/profiler/Profiler.java
index ee224c8..3b87223 100644
--- a/src/main/java/com/google/devtools/build/lib/profiler/Profiler.java
+++ b/src/main/java/com/google/devtools/build/lib/profiler/Profiler.java
@@ -433,11 +433,11 @@
     this.clock = clock;
     this.actionCountStartTime = Duration.ofNanos(clock.nanoTime());
     this.actionCountTimeSeriesRef.set(
-        new TimeSeries(actionCountStartTime, ACTION_COUNT_BUCKET_DURATION));
+        createTimeSeries(actionCountStartTime, ACTION_COUNT_BUCKET_DURATION));
     this.actionCacheCountTimeSeriesRef.set(
-        new TimeSeries(actionCountStartTime, ACTION_COUNT_BUCKET_DURATION));
+        createTimeSeries(actionCountStartTime, ACTION_COUNT_BUCKET_DURATION));
     this.localActionCountTimeSeriesRef.set(
-        new TimeSeries(actionCountStartTime, ACTION_COUNT_BUCKET_DURATION));
+        createTimeSeries(actionCountStartTime, ACTION_COUNT_BUCKET_DURATION));
     this.inflightRpcTimeSeriesMapRef.set(new ConcurrentHashMap<>());
     this.collectTaskHistograms = collectTaskHistograms;
     this.includePrimaryOutput = includePrimaryOutput;
@@ -876,7 +876,7 @@
           var timeSeries =
               inflightRpcTimeSerieMap.computeIfAbsent(
                   description,
-                  (unused) -> new TimeSeries(actionCountStartTime, ACTION_COUNT_BUCKET_DURATION));
+                  (unused) -> createTimeSeries(actionCountStartTime, ACTION_COUNT_BUCKET_DURATION));
           timeSeries.addRange(Duration.ofNanos(startTimeNanos), Duration.ofNanos(endTimeNanos));
         }
       }
@@ -1214,4 +1214,8 @@
   public AsyncProfiler profileAsync(String prefix, String description) {
     return new AsyncProfilerImpl(prefix, description);
   }
+
+  public TimeSeries createTimeSeries(Duration startTime, Duration bucketDuration) {
+    return new TimeSeriesImpl(startTime, bucketDuration);
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/TimeSeries.java b/src/main/java/com/google/devtools/build/lib/profiler/TimeSeries.java
index 79f5b67..bdb07f2 100644
--- a/src/main/java/com/google/devtools/build/lib/profiler/TimeSeries.java
+++ b/src/main/java/com/google/devtools/build/lib/profiler/TimeSeries.java
@@ -13,84 +13,20 @@
 // limitations under the License.
 package com.google.devtools.build.lib.profiler;
 
-import static java.lang.Math.max;
-
 import java.time.Duration;
-import java.util.Arrays;
-import javax.annotation.concurrent.GuardedBy;
 
 /**
  * Converts a set of ranges into a graph by counting the number of ranges that are active at any
  * point in time. Time is split into equal-sized buckets, and we compute one value per bucket. If a
  * range partially overlaps a bucket, then the bucket is incremented by the fraction of overlap.
  */
-public class TimeSeries {
-  private final Duration startTime;
-  private final long bucketSizeMillis;
-  private static final int INITIAL_SIZE = 100;
+public interface TimeSeries {
 
-  @GuardedBy("this")
-  private double[] data = new double[INITIAL_SIZE];
-
-  public TimeSeries(Duration startTime, Duration bucketDuration) {
-    this.startTime = startTime;
-    this.bucketSizeMillis = bucketDuration.toMillis();
-  }
-
-  public void addRange(Duration startTime, Duration endTime) {
-    addRange(startTime, endTime, /* value= */ 1);
-  }
+  /** Adds a new range to the time series, by increasing every affected bucket by 1. */
+  void addRange(Duration startTime, Duration endTime);
 
   /** Adds a new range to the time series, by increasing every affected bucket by value. */
-  public void addRange(Duration rangeStart, Duration rangeEnd, double value) {
-    // Compute times relative to start and their positions in the data array.
-    rangeStart = rangeStart.minus(startTime);
-    rangeEnd = rangeEnd.minus(startTime);
-    int startPosition = (int) (rangeStart.toMillis() / bucketSizeMillis);
-    int endPosition = (int) (rangeEnd.toMillis() / bucketSizeMillis);
+  void addRange(Duration rangeStart, Duration rangeEnd, double value);
 
-    // Assume we add the following range R:
-    // ----------------------------------
-    // |     |ssRRR|RRRRR|Reeee|      |
-    // ----------------------------------
-    // we cannot just add value to each affected bucket but have to correct the values for the first
-    // and last bucket by calculating the size of 's' and 'e'.
-    double missingStartFraction =
-        ((double) rangeStart.minusMillis(bucketSizeMillis * startPosition).toMillis())
-            / bucketSizeMillis;
-    double missingEndFraction =
-        ((double) (bucketSizeMillis * (endPosition + 1) - rangeEnd.toMillis())) / bucketSizeMillis;
-
-    if (startPosition < 0) {
-      startPosition = 0;
-      missingStartFraction = 0;
-    }
-    if (endPosition < startPosition) {
-      endPosition = startPosition;
-      missingEndFraction = 0;
-    }
-
-    synchronized (this) {
-      // Resize data array if necessary so it can at least fit endPosition.
-      if (endPosition >= data.length) {
-        data = Arrays.copyOf(data, max(endPosition + 1, 2 * data.length));
-      }
-
-      // Do the actual update.
-      for (int i = startPosition; i <= endPosition; i++) {
-        double fraction = 1;
-        if (i == startPosition) {
-          fraction -= missingStartFraction;
-        }
-        if (i == endPosition) {
-          fraction -= missingEndFraction;
-        }
-        data[i] += fraction * value;
-      }
-    }
-  }
-
-  public synchronized double[] toDoubleArray(int len) {
-    return Arrays.copyOf(data, len);
-  }
+  double[] toDoubleArray(int len);
 }
diff --git a/src/main/java/com/google/devtools/build/lib/profiler/TimeSeriesImpl.java b/src/main/java/com/google/devtools/build/lib/profiler/TimeSeriesImpl.java
new file mode 100644
index 0000000..04b2af9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/profiler/TimeSeriesImpl.java
@@ -0,0 +1,94 @@
+// Copyright 2025 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.profiler;
+
+import static java.lang.Math.max;
+
+import java.time.Duration;
+import java.util.Arrays;
+import javax.annotation.concurrent.GuardedBy;
+
+/** Implementation of {@link TimeSeries}. */
+public class TimeSeriesImpl implements TimeSeries {
+  private final Duration startTime;
+  private final long bucketSizeMillis;
+  private static final int INITIAL_SIZE = 100;
+
+  @GuardedBy("this")
+  private double[] data = new double[INITIAL_SIZE];
+
+  TimeSeriesImpl(Duration startTime, Duration bucketDuration) {
+    this.startTime = startTime;
+    this.bucketSizeMillis = bucketDuration.toMillis();
+  }
+
+  @Override
+  public void addRange(Duration startTime, Duration endTime) {
+    addRange(startTime, endTime, /* value= */ 1);
+  }
+
+  @Override
+  public void addRange(Duration rangeStart, Duration rangeEnd, double value) {
+    // Compute times relative to start and their positions in the data array.
+    rangeStart = rangeStart.minus(startTime);
+    rangeEnd = rangeEnd.minus(startTime);
+    int startPosition = (int) (rangeStart.toMillis() / bucketSizeMillis);
+    int endPosition = (int) (rangeEnd.toMillis() / bucketSizeMillis);
+
+    // Assume we add the following range R:
+    // ----------------------------------
+    // |     |ssRRR|RRRRR|Reeee|      |
+    // ----------------------------------
+    // we cannot just add value to each affected bucket but have to correct the values for the first
+    // and last bucket by calculating the size of 's' and 'e'.
+    double missingStartFraction =
+        ((double) rangeStart.minusMillis(bucketSizeMillis * startPosition).toMillis())
+            / bucketSizeMillis;
+    double missingEndFraction =
+        ((double) (bucketSizeMillis * (endPosition + 1) - rangeEnd.toMillis())) / bucketSizeMillis;
+
+    if (startPosition < 0) {
+      startPosition = 0;
+      missingStartFraction = 0;
+    }
+    if (endPosition < startPosition) {
+      endPosition = startPosition;
+      missingEndFraction = 0;
+    }
+
+    synchronized (this) {
+      // Resize data array if necessary so it can at least fit endPosition.
+      if (endPosition >= data.length) {
+        data = Arrays.copyOf(data, max(endPosition + 1, 2 * data.length));
+      }
+
+      // Do the actual update.
+      for (int i = startPosition; i <= endPosition; i++) {
+        double fraction = 1;
+        if (i == startPosition) {
+          fraction -= missingStartFraction;
+        }
+        if (i == endPosition) {
+          fraction -= missingEndFraction;
+        }
+        data[i] += fraction * value;
+      }
+    }
+  }
+
+  @Override
+  public synchronized double[] toDoubleArray(int len) {
+    return Arrays.copyOf(data, len);
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/lib/profiler/TimeSeriesTest.java b/src/test/java/com/google/devtools/build/lib/profiler/TimeSeriesTest.java
index b7227df..6ff5c58 100644
--- a/src/test/java/com/google/devtools/build/lib/profiler/TimeSeriesTest.java
+++ b/src/test/java/com/google/devtools/build/lib/profiler/TimeSeriesTest.java
@@ -23,12 +23,12 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
-/** Tests for {@link TimeSeries}. */
+/** Tests for {@link TimeSeriesImpl}. */
 @RunWith(JUnit4.class)
 public final class TimeSeriesTest {
   @Test
   public void testAddRange() {
-    TimeSeries timeSeries = new TimeSeries(Duration.ofMillis(42), Duration.ofMillis(100));
+    TimeSeries timeSeries = new TimeSeriesImpl(Duration.ofMillis(42), Duration.ofMillis(100));
     timeSeries.addRange(Duration.ofMillis(42), Duration.ofMillis(142));
     timeSeries.addRange(Duration.ofMillis(442), Duration.ofMillis(542));
     double[] values = timeSeries.toDoubleArray(5);
@@ -37,7 +37,7 @@
 
   @Test
   public void testAddRangeWithValue() {
-    TimeSeries timeSeries = new TimeSeries(Duration.ofMillis(42), Duration.ofMillis(100));
+    TimeSeries timeSeries = new TimeSeriesImpl(Duration.ofMillis(42), Duration.ofMillis(100));
     timeSeries.addRange(Duration.ofMillis(42), Duration.ofMillis(242), 3);
     timeSeries.addRange(Duration.ofMillis(442), Duration.ofMillis(542), 0.5);
     double[] values = timeSeries.toDoubleArray(5);
@@ -46,7 +46,7 @@
 
   @Test
   public void testAddRangeOverlappingWithValue() {
-    TimeSeries timeSeries = new TimeSeries(Duration.ofMillis(42), Duration.ofMillis(100));
+    TimeSeries timeSeries = new TimeSeriesImpl(Duration.ofMillis(42), Duration.ofMillis(100));
     timeSeries.addRange(Duration.ofMillis(42), Duration.ofMillis(242), 3);
     timeSeries.addRange(Duration.ofMillis(142), Duration.ofMillis(442), 0.5);
     double[] values = timeSeries.toDoubleArray(5);
@@ -55,7 +55,7 @@
 
   @Test
   public void testAddRangeFractions() {
-    TimeSeries timeSeries = new TimeSeries(Duration.ofMillis(42), Duration.ofMillis(100));
+    TimeSeries timeSeries = new TimeSeriesImpl(Duration.ofMillis(42), Duration.ofMillis(100));
     timeSeries.addRange(Duration.ofMillis(92), Duration.ofMillis(267));
     double[] values = timeSeries.toDoubleArray(5);
     assertThat(values).usingTolerance(1.0e-10).containsExactly(0.5, 1, 0.25, 0, 0).inOrder();
@@ -63,7 +63,7 @@
 
   @Test
   public void testAddRangeWithValueFractions() {
-    TimeSeries timeSeries = new TimeSeries(Duration.ofMillis(42), Duration.ofMillis(100));
+    TimeSeries timeSeries = new TimeSeriesImpl(Duration.ofMillis(42), Duration.ofMillis(100));
     timeSeries.addRange(Duration.ofMillis(92), Duration.ofMillis(267), 3);
     double[] values = timeSeries.toDoubleArray(5);
     assertThat(values).usingTolerance(1.0e-10).containsExactly(1.5, 3, 0.75, 0, 0).inOrder();
@@ -71,7 +71,7 @@
 
   @Test
   public void testResize() {
-    TimeSeries timeSeries = new TimeSeries(Duration.ZERO, Duration.ofMillis(100));
+    TimeSeries timeSeries = new TimeSeriesImpl(Duration.ZERO, Duration.ofMillis(100));
     timeSeries.addRange(Duration.ZERO, Duration.ofMillis(100 * 100 + 1), 42);
     double[] values = timeSeries.toDoubleArray(101);
     double[] expected = new double[101];
@@ -83,7 +83,7 @@
   @Test
   public void testParallelism() throws Exception {
     // Define two threads. One is writing 1 on odd places, and another writes 2 on even places.
-    TimeSeries timeSeries = new TimeSeries(Duration.ZERO, Duration.ofMillis(100));
+    TimeSeries timeSeries = new TimeSeriesImpl(Duration.ZERO, Duration.ofMillis(100));
     CountDownLatch latch = new CountDownLatch(2);
     TestThread thread1 =
         new TestThread(