Skip to content

Commit

Permalink
Moved some tests from MDCS to StatsHolderTests
Browse files Browse the repository at this point in the history
Signed-off-by: Peter Alfonsi <[email protected]>
  • Loading branch information
Peter Alfonsi committed Apr 11, 2024
1 parent fddd56e commit c0c7b8e
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 156 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,21 @@

package org.opensearch.common.cache.stats;

import org.opensearch.common.Randomness;
import org.opensearch.common.io.stream.BytesStreamOutput;
import org.opensearch.core.common.bytes.BytesReference;
import org.opensearch.core.common.io.stream.BytesStreamInput;
import org.opensearch.test.OpenSearchTestCase;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

public class MultiDimensionCacheStatsTests extends OpenSearchTestCase {
public void testSerialization() throws Exception {
List<String> dimensionNames = List.of("dim1", "dim2", "dim3");
StatsHolder statsHolder = new StatsHolder(dimensionNames);
Map<String, List<String>> usedDimensionValues = getUsedDimensionValues(statsHolder, 10);
populateStats(statsHolder, usedDimensionValues, 100, 10);
Map<String, List<String>> usedDimensionValues = StatsHolderTests.getUsedDimensionValues(statsHolder, 10);
StatsHolderTests.populateStats(statsHolder, usedDimensionValues, 100, 10);
MultiDimensionCacheStats stats = (MultiDimensionCacheStats) statsHolder.getCacheStats();

BytesStreamOutput os = new BytesStreamOutput();
Expand All @@ -48,158 +42,18 @@ public void testSerialization() throws Exception {
}
}

public void testAddAndGet() throws Exception {
List<String> dimensionNames = List.of("dim1", "dim2", "dim3", "dim4");
StatsHolder statsHolder = new StatsHolder(dimensionNames);
Map<String, List<String>> usedDimensionValues = getUsedDimensionValues(statsHolder, 10);
Map<List<String>, CacheStatsCounter> expected = populateStats(statsHolder, usedDimensionValues, 1000, 10);
MultiDimensionCacheStats stats = (MultiDimensionCacheStats) statsHolder.getCacheStats();

// test the value in the map is as expected for each distinct combination of values
for (List<String> dimensionValues : expected.keySet()) {
CacheStatsCounter expectedCounter = expected.get(dimensionValues);

CacheStatsCounterSnapshot actualStatsHolder = StatsHolderTests.getNode(dimensionValues, statsHolder.getStatsRoot())
.getStatsSnapshot();
CacheStatsCounterSnapshot actualCacheStats = getNode(dimensionValues, stats.getStatsRoot()).getStats();

assertEquals(expectedCounter.snapshot(), actualStatsHolder);
assertEquals(expectedCounter.snapshot(), actualCacheStats);
}

// test gets for total (this also checks sum-of-children logic)
CacheStatsCounter expectedTotal = new CacheStatsCounter();
for (List<String> dims : expected.keySet()) {
expectedTotal.add(expected.get(dims));
}
assertEquals(expectedTotal.snapshot(), stats.getTotalStats());

assertEquals(expectedTotal.getHits(), stats.getTotalHits());
assertEquals(expectedTotal.getMisses(), stats.getTotalMisses());
assertEquals(expectedTotal.getEvictions(), stats.getTotalEvictions());
assertEquals(expectedTotal.getSizeInBytes(), stats.getTotalSizeInBytes());
assertEquals(expectedTotal.getEntries(), stats.getTotalEntries());

assertSumOfChildrenStats(stats.getStatsRoot());
}

public void testEmptyDimsList() throws Exception {
// If the dimension list is empty, the tree should have only the root node containing the total stats.
StatsHolder statsHolder = new StatsHolder(List.of());
Map<String, List<String>> usedDimensionValues = getUsedDimensionValues(statsHolder, 100);
populateStats(statsHolder, usedDimensionValues, 10, 100);
Map<String, List<String>> usedDimensionValues = StatsHolderTests.getUsedDimensionValues(statsHolder, 100);
StatsHolderTests.populateStats(statsHolder, usedDimensionValues, 10, 100);
MultiDimensionCacheStats stats = (MultiDimensionCacheStats) statsHolder.getCacheStats();

MultiDimensionCacheStats.MDCSDimensionNode statsRoot = stats.getStatsRoot();
assertEquals(0, statsRoot.children.size());
assertEquals(stats.getTotalStats(), statsRoot.getStats());
}

private void assertSumOfChildrenStats(MultiDimensionCacheStats.MDCSDimensionNode current) {
if (!current.children.isEmpty()) {
CacheStatsCounter expectedTotal = new CacheStatsCounter();
for (MultiDimensionCacheStats.MDCSDimensionNode child : current.children.values()) {
expectedTotal.add(child.getStats());
}
assertEquals(expectedTotal.snapshot(), current.getStats());
for (MultiDimensionCacheStats.MDCSDimensionNode child : current.children.values()) {
assertSumOfChildrenStats(child);
}
}
}

static Map<String, List<String>> getUsedDimensionValues(StatsHolder statsHolder, int numValuesPerDim) {
Map<String, List<String>> usedDimensionValues = new HashMap<>();
for (int i = 0; i < statsHolder.getDimensionNames().size(); i++) {
List<String> values = new ArrayList<>();
for (int j = 0; j < numValuesPerDim; j++) {
values.add(UUID.randomUUID().toString());
}
usedDimensionValues.put(statsHolder.getDimensionNames().get(i), values);
}
return usedDimensionValues;
}

static Map<List<String>, CacheStatsCounter> populateStats(
StatsHolder statsHolder,
Map<String, List<String>> usedDimensionValues,
int numDistinctValuePairs,
int numRepetitionsPerValue
) throws InterruptedException {
Map<List<String>, CacheStatsCounter> expected = new ConcurrentHashMap<>();

Thread[] threads = new Thread[numDistinctValuePairs];
CountDownLatch countDownLatch = new CountDownLatch(numDistinctValuePairs);
Random rand = Randomness.get();
List<List<String>> dimensionsForThreads = new ArrayList<>();
for (int i = 0; i < numDistinctValuePairs; i++) {
dimensionsForThreads.add(getRandomDimList(statsHolder.getDimensionNames(), usedDimensionValues, true, rand));
int finalI = i;
threads[i] = new Thread(() -> {
Random threadRand = Randomness.get(); // TODO: This always has the same seed for each thread, causing only 1 set of values
List<String> dimensions = dimensionsForThreads.get(finalI);
expected.computeIfAbsent(dimensions, (key) -> new CacheStatsCounter());

for (int j = 0; j < numRepetitionsPerValue; j++) {
int numHitIncrements = threadRand.nextInt(10);
for (int k = 0; k < numHitIncrements; k++) {
statsHolder.incrementHits(dimensions);
expected.get(dimensions).hits.inc();
}
int numMissIncrements = threadRand.nextInt(10);
for (int k = 0; k < numMissIncrements; k++) {
statsHolder.incrementMisses(dimensions);
expected.get(dimensions).misses.inc();
}
int numEvictionIncrements = threadRand.nextInt(10);
for (int k = 0; k < numEvictionIncrements; k++) {
statsHolder.incrementEvictions(dimensions);
expected.get(dimensions).evictions.inc();
}
int numMemorySizeIncrements = threadRand.nextInt(10);
for (int k = 0; k < numMemorySizeIncrements; k++) {
long memIncrementAmount = threadRand.nextInt(5000);
statsHolder.incrementSizeInBytes(dimensions, memIncrementAmount);
expected.get(dimensions).sizeInBytes.inc(memIncrementAmount);
}
int numEntryIncrements = threadRand.nextInt(9) + 1;
for (int k = 0; k < numEntryIncrements; k++) {
statsHolder.incrementEntries(dimensions);
expected.get(dimensions).entries.inc();
}
int numEntryDecrements = threadRand.nextInt(numEntryIncrements);
for (int k = 0; k < numEntryDecrements; k++) {
statsHolder.decrementEntries(dimensions);
expected.get(dimensions).entries.dec();
}
}
countDownLatch.countDown();
});
}
for (Thread thread : threads) {
thread.start();
}
countDownLatch.await();
return expected;
}

private static List<String> getRandomDimList(
List<String> dimensionNames,
Map<String, List<String>> usedDimensionValues,
boolean pickValueForAllDims,
Random rand
) {
List<String> result = new ArrayList<>();
for (String dimName : dimensionNames) {
if (pickValueForAllDims || rand.nextBoolean()) { // if pickValueForAllDims, always pick a value for each dimension, otherwise do
// so 50% of the time
int index = between(0, usedDimensionValues.get(dimName).size() - 1);
result.add(usedDimensionValues.get(dimName).get(index));
}
}
return result;
}

private void getAllPathsInTree(
MultiDimensionCacheStats.MDCSDimensionNode currentNode,
List<String> pathToCurrentNode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,48 @@

package org.opensearch.common.cache.stats;

import org.opensearch.common.Randomness;
import org.opensearch.common.metrics.CounterMetric;
import org.opensearch.test.OpenSearchTestCase;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

import static org.opensearch.common.cache.stats.MultiDimensionCacheStatsTests.getUsedDimensionValues;
import static org.opensearch.common.cache.stats.MultiDimensionCacheStatsTests.populateStats;

public class StatsHolderTests extends OpenSearchTestCase {
// Since StatsHolder does not expose getter methods for aggregating stats,
// we test the incrementing functionality in combination with MultiDimensionCacheStats,
// in MultiDimensionCacheStatsTests.java.
public void testAddAndGet() throws Exception {
List<String> dimensionNames = List.of("dim1", "dim2", "dim3", "dim4");
StatsHolder statsHolder = new StatsHolder(dimensionNames);
Map<String, List<String>> usedDimensionValues = StatsHolderTests.getUsedDimensionValues(statsHolder, 10);
Map<List<String>, CacheStatsCounter> expected = StatsHolderTests.populateStats(statsHolder, usedDimensionValues, 1000, 10);

// test the value in the map is as expected for each distinct combination of values
for (List<String> dimensionValues : expected.keySet()) {
CacheStatsCounter expectedCounter = expected.get(dimensionValues);

CacheStatsCounterSnapshot actualStatsHolder = StatsHolderTests.getNode(dimensionValues, statsHolder.getStatsRoot())
.getStatsSnapshot();
CacheStatsCounterSnapshot actualCacheStats = getNode(dimensionValues, statsHolder.getStatsRoot()).getStatsSnapshot();

assertEquals(expectedCounter.snapshot(), actualStatsHolder);
assertEquals(expectedCounter.snapshot(), actualCacheStats);
}

// Check overall total matches
CacheStatsCounter expectedTotal = new CacheStatsCounter();
for (List<String> dims : expected.keySet()) {
expectedTotal.add(expected.get(dims));
}
assertEquals(expectedTotal.snapshot(), statsHolder.getStatsRoot().getStatsSnapshot());

// Check sum of children stats are correct
assertSumOfChildrenStats(statsHolder.getStatsRoot());
}

public void testReset() throws Exception {
List<String> dimensionNames = List.of("dim1", "dim2");
Expand Down Expand Up @@ -151,4 +179,109 @@ static DimensionNode getNode(List<String> dimensionValues, DimensionNode root) {
}
return current;
}

static Map<List<String>, CacheStatsCounter> populateStats(
StatsHolder statsHolder,
Map<String, List<String>> usedDimensionValues,
int numDistinctValuePairs,
int numRepetitionsPerValue
) throws InterruptedException {
Map<List<String>, CacheStatsCounter> expected = new ConcurrentHashMap<>();

Thread[] threads = new Thread[numDistinctValuePairs];
CountDownLatch countDownLatch = new CountDownLatch(numDistinctValuePairs);
Random rand = Randomness.get();
List<List<String>> dimensionsForThreads = new ArrayList<>();
for (int i = 0; i < numDistinctValuePairs; i++) {
dimensionsForThreads.add(getRandomDimList(statsHolder.getDimensionNames(), usedDimensionValues, true, rand));
int finalI = i;
threads[i] = new Thread(() -> {
Random threadRand = Randomness.get(); // TODO: This always has the same seed for each thread, causing only 1 set of values
List<String> dimensions = dimensionsForThreads.get(finalI);
expected.computeIfAbsent(dimensions, (key) -> new CacheStatsCounter());

for (int j = 0; j < numRepetitionsPerValue; j++) {
int numHitIncrements = threadRand.nextInt(10);
for (int k = 0; k < numHitIncrements; k++) {
statsHolder.incrementHits(dimensions);
expected.get(dimensions).hits.inc();
}
int numMissIncrements = threadRand.nextInt(10);
for (int k = 0; k < numMissIncrements; k++) {
statsHolder.incrementMisses(dimensions);
expected.get(dimensions).misses.inc();
}
int numEvictionIncrements = threadRand.nextInt(10);
for (int k = 0; k < numEvictionIncrements; k++) {
statsHolder.incrementEvictions(dimensions);
expected.get(dimensions).evictions.inc();
}
int numMemorySizeIncrements = threadRand.nextInt(10);
for (int k = 0; k < numMemorySizeIncrements; k++) {
long memIncrementAmount = threadRand.nextInt(5000);
statsHolder.incrementSizeInBytes(dimensions, memIncrementAmount);
expected.get(dimensions).sizeInBytes.inc(memIncrementAmount);
}
int numEntryIncrements = threadRand.nextInt(9) + 1;
for (int k = 0; k < numEntryIncrements; k++) {
statsHolder.incrementEntries(dimensions);
expected.get(dimensions).entries.inc();
}
int numEntryDecrements = threadRand.nextInt(numEntryIncrements);
for (int k = 0; k < numEntryDecrements; k++) {
statsHolder.decrementEntries(dimensions);
expected.get(dimensions).entries.dec();
}
}
countDownLatch.countDown();
});
}
for (Thread thread : threads) {
thread.start();
}
countDownLatch.await();
return expected;
}

private static List<String> getRandomDimList(
List<String> dimensionNames,
Map<String, List<String>> usedDimensionValues,
boolean pickValueForAllDims,
Random rand
) {
List<String> result = new ArrayList<>();
for (String dimName : dimensionNames) {
if (pickValueForAllDims || rand.nextBoolean()) { // if pickValueForAllDims, always pick a value for each dimension, otherwise do
// so 50% of the time
int index = between(0, usedDimensionValues.get(dimName).size() - 1);
result.add(usedDimensionValues.get(dimName).get(index));
}
}
return result;
}

static Map<String, List<String>> getUsedDimensionValues(StatsHolder statsHolder, int numValuesPerDim) {
Map<String, List<String>> usedDimensionValues = new HashMap<>();
for (int i = 0; i < statsHolder.getDimensionNames().size(); i++) {
List<String> values = new ArrayList<>();
for (int j = 0; j < numValuesPerDim; j++) {
values.add(UUID.randomUUID().toString());
}
usedDimensionValues.put(statsHolder.getDimensionNames().get(i), values);
}
return usedDimensionValues;
}

private void assertSumOfChildrenStats(DimensionNode current) {
if (!current.children.isEmpty()) {
CacheStatsCounter expectedTotal = new CacheStatsCounter();
for (DimensionNode child : current.children.values()) {
expectedTotal.add(child.getStatsSnapshot());
}
assertEquals(expectedTotal.snapshot(), current.getStatsSnapshot());
for (DimensionNode child : current.children.values()) {
assertSumOfChildrenStats(child);
}
}
}
}

0 comments on commit c0c7b8e

Please sign in to comment.