Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.exporter.otlp.profiles;

import io.opentelemetry.exporter.otlp.internal.data.ImmutableSampleData;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;

/**
* Assembles a collection of state observations into a collection of SampleData objects i.e. proto
* Sample messages.
*
* <p>Observations (samples or traces from a profiler) having the same key can be merged to save
* space without loss of fidelity. On the wire, a single Sample, modelled in the API as a SampleData
* object, comprises shared fields (the key) and per-occurrence fields (the value and timestamp).
* This class maps the raw observations to the aggregations.
*
* <p>This class is not threadsafe and must be externally synchronized.
*/
public class SampleCompositionBuilder {

private final Map<SampleCompositionKey, SampleCompositionValue> map = new HashMap<>();

/**
* Constructs a new collection of SampleData instances based on the builder's value.
*
* @return a new {@code List<SampleData>}
*/
public List<SampleData> build() {
List<SampleData> result = new ArrayList<>(map.size());
for (Map.Entry<SampleCompositionKey, SampleCompositionValue> entry : map.entrySet()) {
SampleCompositionKey key = entry.getKey();
SampleCompositionValue value = entry.getValue();
SampleData sampleData =
ImmutableSampleData.create(
key.getStackIndex(),
value.getValues(),
key.getAttributeIndices(),
key.getLinkIndex(),
value.getTimestamps());
result.add(sampleData);
}

return result;
}

/**
* Adds a new observation to the collection.
*
* @param key the shared ('primary key') fields of the observation.
* @param value the observed data point.
* @param timestamp the time of the observation.
*/
public void add(SampleCompositionKey key, @Nullable Long value, @Nullable Long timestamp) {
SampleCompositionValue v = map.computeIfAbsent(key, key1 -> new SampleCompositionValue());
v.add(value, timestamp);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.exporter.otlp.profiles;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javax.annotation.concurrent.Immutable;

/**
* A SampleCompositionKey represents the identity portion of an aggregation of observed data.
* Observations (samples) having the same key can be merged to save space without loss of fidelity.
*/
@Immutable
public class SampleCompositionKey {

// on the wire, a Sample's identity (i.e. 'primary key') is the tuple of
// {stack_index, sorted(attribute_indices), link_index}
private final int stackIndex;
private final List<Integer> attributeIndices;
private final int linkIndex;

public SampleCompositionKey(int stackIndex, List<Integer> attributeIndices, int linkIndex) {
this.stackIndex = stackIndex;
List<Integer> tmp = new ArrayList<>(attributeIndices);
Collections.sort(tmp);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any restrictions on duplicate values in the indices here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the spec level, no. Realistically the user level API will probably be Attributes which has Set semantics. I expect the building flow will be passing an Attributes to the dictionary to get back the List, rather than hand crafting the list, but here, as with e.g. the dictionary not enforcing referential integrity, it is possible to construct bad pointers if you try.
It's trickier than it looks to fully validate input here, as merely checking for duplicate Integers isn't sufficient to check for duplicate keys, because the underlying dictionary entries deliberately use the whole key+value+type attribute tuple, not just the key, so you have to dereference the pointers to extract the key, which you can't do without a handle on the dictionary. Likewise the stackIndex and linkIndex can't be range checked without the dictionary.
I'm not adverse to putting more safeguards in, but the current design philosophy leans more towards flexibility for users who know what they are doing, as it's focussed on use by a small set of profiler libraries than than a wide audience of application developers that something like the metrics API has. There is some help baked in, such as the sort operation here to enforce the right equality semantics, but I'm not attempting to bullet proof it.

this.attributeIndices = Collections.unmodifiableList(tmp);
this.linkIndex = linkIndex;
}

public int getStackIndex() {
return stackIndex;
}

public List<Integer> getAttributeIndices() {
return attributeIndices;
}

public int getLinkIndex() {
return linkIndex;
}

@Override
public boolean equals(Object o) {
if (!(o instanceof SampleCompositionKey)) {
return false;
}
SampleCompositionKey that = (SampleCompositionKey) o;
return stackIndex == that.stackIndex
&& linkIndex == that.linkIndex
&& Objects.equals(attributeIndices, that.attributeIndices);
}

@Override
public int hashCode() {
return Objects.hash(stackIndex, attributeIndices, linkIndex);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.exporter.otlp.profiles;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;

/**
* A SampleCompositionValue represents the per-observation parts of an aggregation of observed data.
* Observations (samples) having the same key can be merged by appending their distinct fields to
* the composed value.
*
* <p>This class is not threadsafe and must be externally synchronized.
*/
public class SampleCompositionValue {

private final List<Long> values = new ArrayList<>();
private final List<Long> timestamps = new ArrayList<>();

public List<Long> getValues() {
return Collections.unmodifiableList(values);
}

public List<Long> getTimestamps() {
return Collections.unmodifiableList(timestamps);
}

/**
* Add a new observation to the collection.
*
* <p>Note that, whilst not enforced by the API, it is required that all observations in a
* collection share the same 'shape'. That is, they have either a value without timestamp, a
* timestamp without value, or both timestamp and value. Thus each array (values, timestamps) in
* the collection is either zero length, or the same length as the other.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be simple enough to enforce in this method, right? Would it be worth doing, and rejecting observations that aren't conformant?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's a problem in practice, as the data isn't hand crafted, it's from e.g. JFR which should be generating uniform structures already. However, I may add checks once the spec is settled - right now we're still debating how to handle non-timestamped values see here, here and here

*
* @param value the observed data point.
* @param timestamp the time of the observation.
*/
public void add(@Nullable Long value, @Nullable Long timestamp) {
if (value != null) {
values.add(value);
}
if (timestamp != null) {
timestamps.add(timestamp);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.exporter.otlp.profiles;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class SampleCompositionTest {

SampleCompositionBuilder sampleCompositionBuilder;

@BeforeEach
void setUp() {
sampleCompositionBuilder = new SampleCompositionBuilder();
}

@Test
public void empty() {
assertThat(sampleCompositionBuilder.build()).isEmpty();
}

@Test
public void keyEquality() {
SampleCompositionKey a;
SampleCompositionKey b;

a = new SampleCompositionKey(1, listOf(2, 3), 4);
b = new SampleCompositionKey(1, listOf(2, 3), 4);
assertThat(a).isEqualTo(b);
assertThat(a.hashCode()).isEqualTo(b.hashCode());

b = new SampleCompositionKey(1, listOf(3, 2), 4);
assertThat(a).isEqualTo(b);
assertThat(a.hashCode()).isEqualTo(b.hashCode());

b = new SampleCompositionKey(2, listOf(2, 3), 4);
assertThat(a).isNotEqualTo(b);
}

@Test
public void valueElidesNulls() {
SampleCompositionValue v = new SampleCompositionValue();
v.add(1L, 1L);
v.add(null, 2L);
v.add(2L, null);
assertThat(v.getValues().size()).isEqualTo(2);
assertThat(v.getTimestamps().size()).isEqualTo(2);
}

@Test
public void isAggregatingSameKey() {
SampleCompositionKey sampleCompositionKey =
new SampleCompositionKey(0, Collections.emptyList(), 0);
sampleCompositionBuilder.add(sampleCompositionKey, 1L, 1L);
sampleCompositionBuilder.add(sampleCompositionKey, 2L, 2L);

List<SampleData> sampleDataList = sampleCompositionBuilder.build();
assertThat(sampleDataList).size().isEqualTo(1);
assertThat(sampleDataList.get(0).getTimestamps().size()).isEqualTo(2);
assertThat(sampleDataList.get(0).getValues().size()).isEqualTo(2);
}

@Test
public void isNotAggregatingDifferentKey() {
SampleCompositionKey keyA = new SampleCompositionKey(1, Collections.emptyList(), 0);
sampleCompositionBuilder.add(keyA, 1L, 1L);
SampleCompositionKey keyB = new SampleCompositionKey(2, Collections.emptyList(), 0);
sampleCompositionBuilder.add(keyB, 2L, 2L);

List<SampleData> sampleDataList = sampleCompositionBuilder.build();
assertThat(sampleDataList).size().isEqualTo(2);
assertThat(sampleDataList.get(0).getTimestamps().size()).isEqualTo(1);
assertThat(sampleDataList.get(1).getTimestamps().size()).isEqualTo(1);
}

private static <T> List<T> listOf(T a, T b) {
ArrayList<T> list = new ArrayList<>();
list.add(a);
list.add(b);
return Collections.unmodifiableList(list);
}
}
Loading