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(