diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml index b37e0c35ca3..5b755d52d9a 100644 --- a/benchmarks/pom.xml +++ b/benchmarks/pom.xml @@ -51,6 +51,11 @@ druid-processing ${project.parent.version} + + io.druid + druid-server + ${project.parent.version} + com.github.wnameless json-flattener diff --git a/benchmarks/src/main/java/io/druid/server/coordinator/CostBalancerStrategyBenchmark.java b/benchmarks/src/main/java/io/druid/server/coordinator/CostBalancerStrategyBenchmark.java new file mode 100644 index 00000000000..8b6275dc8be --- /dev/null +++ b/benchmarks/src/main/java/io/druid/server/coordinator/CostBalancerStrategyBenchmark.java @@ -0,0 +1,102 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you 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 io.druid.server.coordinator; + +import io.druid.timeline.DataSegment; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +@State(Scope.Benchmark) +public class CostBalancerStrategyBenchmark +{ + private final static DateTime t0 = new DateTime("2016-01-01T01:00:00Z"); + + private List segments; + private DataSegment segment; + + int x1 = 2; + int y0 = 3; + int y1 = 4; + + int n = 10000; + + @Setup + public void setupDummyCluster() + { + segment = createSegment(t0); + + Random r = new Random(1234); + segments = new ArrayList<>(n); + for(int i = 0; i < n; ++i) { + final DateTime t = t0.minusHours(r.nextInt(365 * 24) - 365*12); + segments.add(createSegment(t)); + } + } + + DataSegment createSegment(DateTime t) + { + return new DataSegment( + "test", + new Interval(t, t.plusHours(1)), + "v1", + null, + null, + null, + null, + 0, + 0 + ); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + @Fork(1) + public double measureCostStrategySingle() throws InterruptedException + { + double totalCost = 0; + for(DataSegment s : segments) { + totalCost += CostBalancerStrategy.computeJointSegmentsCost(segment, s); + } + return totalCost; + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + @Fork(1) + public double measureIntervalPenalty() throws InterruptedException + { + return CostBalancerStrategy.intervalCost(x1, y0, y1); + } +} diff --git a/pom.xml b/pom.xml index 13fc667a424..ebca25b8b2d 100644 --- a/pom.xml +++ b/pom.xml @@ -556,6 +556,11 @@ derbyclient 10.11.1.1 + + org.apache.commons + commons-math3 + 3.6.1 + diff --git a/server/pom.xml b/server/pom.xml index 42f2dbda646..a1ca444673d 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -173,6 +173,10 @@ org.apache.derby derbyclient + + org.apache.commons + commons-math3 + diff --git a/server/src/main/java/io/druid/server/coordinator/CostBalancerStrategy.java b/server/src/main/java/io/druid/server/coordinator/CostBalancerStrategy.java index 9373db2b421..c9565a6586e 100644 --- a/server/src/main/java/io/druid/server/coordinator/CostBalancerStrategy.java +++ b/server/src/main/java/io/druid/server/coordinator/CostBalancerStrategy.java @@ -19,44 +19,162 @@ package io.druid.server.coordinator; +import com.google.common.base.Predicates; +import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; import com.metamx.common.Pair; import com.metamx.emitter.EmittingLogger; import io.druid.timeline.DataSegment; -import org.joda.time.DateTime; +import org.apache.commons.math3.util.FastMath; import org.joda.time.Interval; import java.util.List; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; public class CostBalancerStrategy implements BalancerStrategy { private static final EmittingLogger log = new EmittingLogger(CostBalancerStrategy.class); - private static final long DAY_IN_MILLIS = 1000 * 60 * 60 * 24; - private static final long SEVEN_DAYS_IN_MILLIS = 7 * DAY_IN_MILLIS; - private static final long THIRTY_DAYS_IN_MILLIS = 30 * DAY_IN_MILLIS; - private final long referenceTimestamp; - private final ListeningExecutorService exec; - public static long gapMillis(Interval interval1, Interval interval2) + private static final double HALF_LIFE = 24.0; // cost function half-life in hours + static final double LAMBDA = Math.log(2) / HALF_LIFE; + static final double INV_LAMBDA_SQUARE = 1 / (LAMBDA * LAMBDA); + + private static final double MILLIS_IN_HOUR = 3_600_000.0; + private static final double MILLIS_FACTOR = MILLIS_IN_HOUR / LAMBDA; + + /** + * This defines the unnormalized cost function between two segments. + * + * See https://github.com/druid-io/druid/pull/2972 for more details about the cost function. + * + * intervalCost: segments close together are more likely to be queried together + * + * multiplier: if two segments belong to the same data source, they are more likely to be involved + * in the same queries + * + * @param segmentA The first DataSegment. + * @param segmentB The second DataSegment. + * + * @return the joint cost of placing the two DataSegments together on one node. + */ + public static double computeJointSegmentsCost(final DataSegment segmentA, final DataSegment segmentB) { - if (interval1.getStartMillis() > interval2.getEndMillis()) { - return interval1.getStartMillis() - interval2.getEndMillis(); - } else if (interval2.getStartMillis() > interval1.getEndMillis()) { - return interval2.getStartMillis() - interval1.getEndMillis(); - } else { + final Interval intervalA = segmentA.getInterval(); + final Interval intervalB = segmentB.getInterval(); + + final double t0 = intervalA.getStartMillis(); + final double t1 = (intervalA.getEndMillis() - t0) / MILLIS_FACTOR; + final double start = (intervalB.getStartMillis() - t0) / MILLIS_FACTOR; + final double end = (intervalB.getEndMillis() - t0) / MILLIS_FACTOR; + + // constant cost-multiplier for segments of the same datsource + final double multiplier = segmentA.getDataSource().equals(segmentB.getDataSource()) ? 2.0 : 1.0; + + return INV_LAMBDA_SQUARE * intervalCost(t1, start, end) * multiplier; + } + + /** + * Computes the joint cost of two intervals X = [x_0 = 0, x_1) and Y = [y_0, y_1) + * + * cost(X, Y) = \int_{x_0}^{x_1} \int_{y_0}^{y_1} e^{-\lambda |x-y|}dxdy $$ + * + * lambda = 1 in this particular implementation + * + * Other values of lambda can be calculated by multiplying inputs by lambda + * and multiplying the result by 1 / lambda ^ 2 + * + * Interval start and end are all relative to x_0. + * Therefore this function assumes x_0 = 0, x1 >= 0, and y1 > y0 + * + * @param x1 end of interval X + * @param y0 start of interval Y + * @param y1 end o interval Y + * + * @return joint cost of X and Y + */ + public static double intervalCost(double x1, double y0, double y1) + { + if (x1 == 0 || y1 == y0) { return 0; } + + // cost(X, Y) = cost(Y, X), so we swap X and Y to + // have x_0 <= y_0 and simplify the calculations below + if (y0 < 0) { + // swap X and Y + double tmp = x1; + x1 = y1 - y0; + y1 = tmp - y0; + y0 = -y0; + } + + // since x_0 <= y_0, Y must overlap X if y_0 < x_1 + if (y0 < x1) { + /** + * We have two possible cases of overlap: + * + * X = [ A )[ B )[ C ) or [ A )[ B ) + * Y = [ ) [ )[ C ) + * + * A is empty if y0 = 0 + * C is empty if y1 = x1 + * + * cost(X, Y) = cost(A, Y) + cost(B, C) + cost(B, B) + * + * cost(A, Y) and cost(B, C) can be calculated using the non-overlapping case, + * which reduces the overlapping case to computing + * + * cost(B, B) = \int_0^{\beta} \int_{0}^{\beta} e^{-|x-y|}dxdy + * = 2 \cdot (\beta + e^{-\beta} - 1) + * + * where \beta is the length of interval B + * + */ + final double beta; // b1 - y0, length of interval B + final double gamma; // c1 - y0, length of interval C + if (y1 <= x1) { + beta = y1 - y0; + gamma = x1 - y0; + } else { + beta = x1 - y0; + gamma = y1 - y0; + } + return intervalCost(y0, y0, y1) + // cost(A, Y) + intervalCost(beta, beta, gamma) + // cost(B, C) + 2 * (beta + FastMath.exp(-beta) - 1); // cost(B, B) + } else { + /** + * In the case where there is no overlap: + * + * Given that x_0 <= y_0, + * then x <= y must be true for all x in [x_0, x_1] and y in [y_0, y_1). + * + * therefore, + * + * cost(X, Y) = \int_0^{x_1} \int_{y_0}^{y_1} e^{-|x-y|} dxdy + * = \int_0^{x_1} \int_{y_0}^{y_1} e^{x-y} dxdy + * = (e^{-y_1} - e^{-y_0}) - (e^{x_1-y_1} - e^{x_1-y_0}) + * + * Note, this expression could be further reduced by factoring out (e^{x_1} - 1), + * but we prefer to keep the smaller values x_1 - y_0 and x_1 - y_1 in the exponent + * to avoid numerical overflow caused by calculating e^{x_1} + */ + final double exy0 = FastMath.exp(x1 - y0); + final double exy1 = FastMath.exp(x1 - y1); + final double ey0 = FastMath.exp(0f - y0); + final double ey1 = FastMath.exp(0f - y1); + + return (ey1 - ey0) - (exy1 - exy0); + } } - public CostBalancerStrategy(DateTime referenceTimestamp, ListeningExecutorService exec) + private final ListeningExecutorService exec; + + public CostBalancerStrategy(ListeningExecutorService exec) { - this.referenceTimestamp = referenceTimestamp.getMillis(); this.exec = exec; } @@ -81,52 +199,16 @@ public class CostBalancerStrategy implements BalancerStrategy return chooseBestServer(proposalSegment, serverHolders, true).rhs; } - /** - * This defines the unnormalized cost function between two segments. There is a base cost given by - * the minimum size of the two segments and additional penalties. - * recencyPenalty: it is more likely that recent segments will be queried together - * dataSourcePenalty: if two segments belong to the same data source, they are more likely to be involved - * in the same queries - * gapPenalty: it is more likely that segments close together in time will be queried together - * - * @param segment1 The first DataSegment. - * @param segment2 The second DataSegment. - * - * @return The joint cost of placing the two DataSegments together on one node. - */ - public double computeJointSegmentCosts(final DataSegment segment1, final DataSegment segment2) + static double computeJointSegmentsCost(final DataSegment segment, final Iterable segmentSet) { - final long gapMillis = gapMillis(segment1.getInterval(), segment2.getInterval()); - - final double baseCost = Math.min(segment1.getSize(), segment2.getSize()); - double recencyPenalty = 1; - double dataSourcePenalty = 1; - double gapPenalty = 1; - - if (segment1.getDataSource().equals(segment2.getDataSource())) { - dataSourcePenalty = 2; + double totalCost = 0; + for (DataSegment s : segmentSet) { + totalCost += computeJointSegmentsCost(segment, s); } - - double segment1diff = referenceTimestamp - segment1.getInterval().getEndMillis(); - double segment2diff = referenceTimestamp - segment2.getInterval().getEndMillis(); - if (segment1diff < SEVEN_DAYS_IN_MILLIS && segment2diff < SEVEN_DAYS_IN_MILLIS) { - recencyPenalty = (2 - segment1diff / SEVEN_DAYS_IN_MILLIS) * (2 - segment2diff / SEVEN_DAYS_IN_MILLIS); - } - - /** gap is null if the two segment intervals overlap or if they're adjacent */ - if (gapMillis == 0) { - gapPenalty = 2; - } else { - if (gapMillis < THIRTY_DAYS_IN_MILLIS) { - gapPenalty = 2 - gapMillis / THIRTY_DAYS_IN_MILLIS; - } - } - - final double cost = baseCost * recencyPenalty * dataSourcePenalty * gapPenalty; - - return cost; + return totalCost; } + public BalancerSegmentHolder pickSegmentToMove(final List serverHolders) { ReservoirSegmentSampler sampler = new ReservoirSegmentSampler(); @@ -144,11 +226,9 @@ public class CostBalancerStrategy implements BalancerStrategy { double cost = 0; for (ServerHolder server : serverHolders) { - DataSegment[] segments = server.getServer().getSegments().values().toArray(new DataSegment[]{}); - for (int i = 0; i < segments.length; ++i) { - for (int j = i; j < segments.length; ++j) { - cost += computeJointSegmentCosts(segments[i], segments[j]); - } + Iterable segments = server.getServer().getSegments().values(); + for (DataSegment s : segments) { + cost += computeJointSegmentsCost(s, segments); } } return cost; @@ -169,7 +249,7 @@ public class CostBalancerStrategy implements BalancerStrategy double cost = 0; for (ServerHolder server : serverHolders) { for (DataSegment segment : server.getServer().getSegments().values()) { - cost += computeJointSegmentCosts(segment, segment); + cost += computeJointSegmentsCost(segment, segment); } } return cost; @@ -211,17 +291,20 @@ public class CostBalancerStrategy implements BalancerStrategy } /** The contribution to the total cost of a given server by proposing to move the segment to that server is... */ - double cost = 0f; + double cost = 0d; + /** the sum of the costs of other (exclusive of the proposalSegment) segments on the server */ - for (DataSegment segment : server.getServer().getSegments().values()) { - if (!proposalSegment.equals(segment)) { - cost += computeJointSegmentCosts(proposalSegment, segment); - } - } + cost += computeJointSegmentsCost( + proposalSegment, + Iterables.filter( + server.getServer().getSegments().values(), + Predicates.not(Predicates.equalTo(proposalSegment)) + ) + ); + /** plus the costs of segments that will be loaded */ - for (DataSegment segment : server.getPeon().getSegmentsToLoad()) { - cost += computeJointSegmentCosts(proposalSegment, segment); - } + cost += computeJointSegmentsCost(proposalSegment, server.getPeon().getSegmentsToLoad()); + return cost; } return Double.POSITIVE_INFINITY; diff --git a/server/src/main/java/io/druid/server/coordinator/CostBalancerStrategyFactory.java b/server/src/main/java/io/druid/server/coordinator/CostBalancerStrategyFactory.java index bbaebe6c7cd..e853302ae31 100644 --- a/server/src/main/java/io/druid/server/coordinator/CostBalancerStrategyFactory.java +++ b/server/src/main/java/io/druid/server/coordinator/CostBalancerStrategyFactory.java @@ -35,9 +35,9 @@ public class CostBalancerStrategyFactory implements BalancerStrategyFactory } @Override - public BalancerStrategy createBalancerStrategy(DateTime referenceTimestamp) + public CostBalancerStrategy createBalancerStrategy(DateTime referenceTimestamp) { - return new CostBalancerStrategy(referenceTimestamp, exec); + return new CostBalancerStrategy(exec); } @Override diff --git a/server/src/test/java/io/druid/server/coordination/CostBalancerStrategyBenchmark.java b/server/src/test/java/io/druid/server/coordinator/CostBalancerStrategyBenchmark.java similarity index 75% rename from server/src/test/java/io/druid/server/coordination/CostBalancerStrategyBenchmark.java rename to server/src/test/java/io/druid/server/coordinator/CostBalancerStrategyBenchmark.java index 0892f7a25c8..4974fb563aa 100644 --- a/server/src/test/java/io/druid/server/coordination/CostBalancerStrategyBenchmark.java +++ b/server/src/test/java/io/druid/server/coordinator/CostBalancerStrategyBenchmark.java @@ -1,29 +1,26 @@ /* * Licensed to Metamarkets Group Inc. (Metamarkets) under one - * or more contributor license agreements. See the NOTICE file + * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information - * regarding copyright ownership. Metamarkets licenses this file + * regarding copyright ownership. Metamarkets licenses this file * to you 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 + * with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 + * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ -package io.druid.server.coordination; +package io.druid.server.coordinator; import com.carrotsearch.junitbenchmarks.AbstractBenchmark; import com.carrotsearch.junitbenchmarks.BenchmarkOptions; -import io.druid.server.coordinator.CostBalancerStrategy; -import io.druid.server.coordinator.CostBalancerStrategyFactory; -import io.druid.server.coordinator.ServerHolder; import io.druid.timeline.DataSegment; import org.joda.time.DateTime; import org.joda.time.Interval; @@ -58,7 +55,7 @@ public class CostBalancerStrategyBenchmark extends AbstractBenchmark public CostBalancerStrategyBenchmark(CostBalancerStrategyFactory factory) { - this.strategy = (CostBalancerStrategy) factory.createBalancerStrategy(DateTime.now()); + this.strategy = factory.createBalancerStrategy(DateTime.now()); } private static List serverHolderList; @@ -99,16 +96,4 @@ public class CostBalancerStrategyBenchmark extends AbstractBenchmark } sum = diff; } - - @BenchmarkOptions(warmupRounds = 1000, benchmarkRounds = 1000000) - @Test - public void testBalancerGapMillis() - { - long diff = 0; - for (int i = 0; i < 1000; i++) { - diff = diff + CostBalancerStrategy.gapMillis(interval1, interval2); - } - sum = diff; - } - } diff --git a/server/src/test/java/io/druid/server/coordination/CostBalancerStrategyTest.java b/server/src/test/java/io/druid/server/coordinator/CostBalancerStrategyTest.java similarity index 68% rename from server/src/test/java/io/druid/server/coordination/CostBalancerStrategyTest.java rename to server/src/test/java/io/druid/server/coordinator/CostBalancerStrategyTest.java index be0eec6ce5b..16184cf180d 100644 --- a/server/src/test/java/io/druid/server/coordination/CostBalancerStrategyTest.java +++ b/server/src/test/java/io/druid/server/coordinator/CostBalancerStrategyTest.java @@ -1,23 +1,23 @@ /* * Licensed to Metamarkets Group Inc. (Metamarkets) under one - * or more contributor license agreements. See the NOTICE file + * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information - * regarding copyright ownership. Metamarkets licenses this file + * regarding copyright ownership. Metamarkets licenses this file * to you 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 + * with the License. You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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 + * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ -package io.druid.server.coordination; +package io.druid.server.coordinator; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; @@ -25,18 +25,12 @@ import com.google.common.collect.Maps; import com.google.common.util.concurrent.MoreExecutors; import io.druid.client.ImmutableDruidDataSource; import io.druid.client.ImmutableDruidServer; -import io.druid.concurrent.Execs; -import io.druid.server.coordinator.BalancerStrategy; -import io.druid.server.coordinator.CostBalancerStrategy; -import io.druid.server.coordinator.LoadQueuePeonTester; -import io.druid.server.coordinator.ServerHolder; +import io.druid.server.coordination.DruidServerMetadata; import io.druid.timeline.DataSegment; import org.easymock.EasyMock; import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; import org.joda.time.Interval; import org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import java.util.List; @@ -45,7 +39,7 @@ import java.util.concurrent.Executors; public class CostBalancerStrategyTest { - private static final Interval day = DateTime.now().toDateMidnight().toInterval(); + private static final Interval day = new Interval("2015-01-01T00/2015-01-01T01"); /** * Create Druid cluster with serverCount servers having maxSegments segments each, and 1 server with 98 segment @@ -125,9 +119,7 @@ public class CostBalancerStrategyTest List serverHolderList = setupDummyCluster(10, 20); DataSegment segment = getSegment(1000); - final DateTime referenceTimestamp = new DateTime("2014-01-01"); BalancerStrategy strategy = new CostBalancerStrategy( - referenceTimestamp, MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(4)) ); ServerHolder holder = strategy.findNewSegmentHomeReplicator(segment, serverHolderList); @@ -143,7 +135,6 @@ public class CostBalancerStrategyTest final DateTime referenceTimestamp = new DateTime("2014-01-01"); BalancerStrategy strategy = new CostBalancerStrategy( - referenceTimestamp, MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1)) ); ServerHolder holder = strategy.findNewSegmentHomeReplicator(segment, serverHolderList); @@ -156,10 +147,9 @@ public class CostBalancerStrategyTest { DateTime referenceTime = new DateTime("2014-01-01T00:00:00"); CostBalancerStrategy strategy = new CostBalancerStrategy( - referenceTime, MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(4)) ); - double segmentCost = strategy.computeJointSegmentCosts( + double segmentCost = strategy.computeJointSegmentsCost( getSegment( 100, "DUMMY", @@ -172,12 +162,54 @@ public class CostBalancerStrategyTest 101, "DUMMY", new Interval( - referenceTime.minusDays(2), - referenceTime.minusDays(2).plusHours(1) + referenceTime.minusHours(2), + referenceTime.minusHours(2).plusHours(1) ) ) ); - Assert.assertEquals(138028.62811791385d, segmentCost, 0); + + Assert.assertEquals( + CostBalancerStrategy.INV_LAMBDA_SQUARE * CostBalancerStrategy.intervalCost( + 1 * CostBalancerStrategy.LAMBDA, + -2 * CostBalancerStrategy.LAMBDA, + -1 * CostBalancerStrategy.LAMBDA + ) * 2, + segmentCost, 1e-6); } + @Test + public void testIntervalCost() throws Exception + { + // additivity + Assert.assertEquals(CostBalancerStrategy.intervalCost(1, 1, 3), + CostBalancerStrategy.intervalCost(1, 1, 2) + + CostBalancerStrategy.intervalCost(1, 2, 3), 1e-6); + + Assert.assertEquals(CostBalancerStrategy.intervalCost(2, 1, 3), + CostBalancerStrategy.intervalCost(2, 1, 2) + + CostBalancerStrategy.intervalCost(2, 2, 3), 1e-6); + + Assert.assertEquals(CostBalancerStrategy.intervalCost(3, 1, 2), + CostBalancerStrategy.intervalCost(1, 1, 2) + + CostBalancerStrategy.intervalCost(1, 0, 1) + + CostBalancerStrategy.intervalCost(1, 1, 2), 1e-6); + + // no overlap [0, 1) [1, 2) + Assert.assertEquals(0.3995764, CostBalancerStrategy.intervalCost(1, 1, 2), 1e-6); + // no overlap [0, 1) [-1, 0) + Assert.assertEquals(0.3995764, CostBalancerStrategy.intervalCost(1, -1, 0), 1e-6); + + // exact overlap [0, 1), [0, 1) + Assert.assertEquals(0.7357589, CostBalancerStrategy.intervalCost(1, 0, 1), 1e-6); + // exact overlap [0, 2), [0, 2) + Assert.assertEquals(2.270671, CostBalancerStrategy.intervalCost(2, 0, 2), 1e-6); + // partial overlap [0, 2), [1, 3) + Assert.assertEquals(1.681908, CostBalancerStrategy.intervalCost(2, 1, 3), 1e-6); + // partial overlap [0, 2), [1, 2) + Assert.assertEquals(1.135335, CostBalancerStrategy.intervalCost(2, 1, 2), 1e-6); + // partial overlap [0, 2), [0, 1) + Assert.assertEquals(1.135335, CostBalancerStrategy.intervalCost(2, 0, 1), 1e-6); + // partial overlap [0, 3), [1, 2) + Assert.assertEquals(1.534912, CostBalancerStrategy.intervalCost(3, 1, 2), 1e-6); + } }