Skip to content
Draft
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
Expand Up @@ -5,6 +5,7 @@
import datadog.trace.api.featureflag.FeatureFlaggingGateway;
import datadog.trace.api.featureflag.exposure.ExposureEvent;
import datadog.trace.api.featureflag.exposure.Subject;
import datadog.trace.api.featureflag.flagevaluation.FlagEvalEventMemoryEstimator;
import datadog.trace.api.featureflag.ufc.v1.Allocation;
import datadog.trace.api.featureflag.ufc.v1.ConditionConfiguration;
import datadog.trace.api.featureflag.ufc.v1.ConditionOperator;
Expand Down Expand Up @@ -826,11 +827,18 @@ static String truncationReasonTag(final int reasonMask) {
static final class CopyResult {
final Map<String, Object> attrs;

/** Conservative retained-byte estimate for attrs, calculated during the bounded copy. */
final long estimatedRetainedBytes;

/** Non-null when at least one cap fired; ready to use as the "reason:..." tag value. */
final String truncatedReason;

CopyResult(final Map<String, Object> attrs, final String truncatedReason) {
CopyResult(
final Map<String, Object> attrs,
final long estimatedRetainedBytes,
final String truncatedReason) {
this.attrs = attrs;
this.estimatedRetainedBytes = estimatedRetainedBytes;
this.truncatedReason = truncatedReason;
}
}
Expand All @@ -857,27 +865,30 @@ static final class CopyResult {
*/
static CopyResult copyPrunedContext(final EvaluationContext context) {
if (context == null) {
return new CopyResult(Collections.emptyMap(), null);
return new CopyResult(Collections.emptyMap(), 0, null);
}
final Set<String> keys = context.keySet();
if (keys.isEmpty()) {
return new CopyResult(Collections.emptyMap(), null);
return new CopyResult(Collections.emptyMap(), 0, null);
}
final HashMap<String, Object> out = new HashMap<>();
final Set<Object> seen = Collections.newSetFromMap(new IdentityHashMap<>());
final int[] reasonMask = {0};
final CopyState state = new CopyState(FlagEvalEventMemoryEstimator.contextMapRetainedBytes());
for (final String key : keys) {
if (out.size() >= MAX_CONTEXT_FIELDS) {
reasonMask[0] |= REASON_MAX_CONTEXT_FIELDS;
state.reasonMask |= REASON_MAX_CONTEXT_FIELDS;
break;
}
if (EvaluationContext.TARGETING_KEY.equals(key)) {
continue;
}
copyPrunedValue(out, key, context.getValue(key), seen, 0, reasonMask);
copyPrunedValue(out, key, context.getValue(key), seen, 0, state);
}
final Map<String, Object> attrs = out.isEmpty() ? Collections.emptyMap() : out;
return new CopyResult(attrs, truncationReasonTag(reasonMask[0]));
return new CopyResult(
attrs,
out.isEmpty() ? 0 : state.estimatedRetainedBytes,
truncationReasonTag(state.reasonMask));
}

private static void copyPrunedValue(
Expand All @@ -886,84 +897,100 @@ private static void copyPrunedValue(
final Value value,
final Set<Object> seen,
final int depth,
final int[] reasonMask) {
final CopyState state) {
if (out.size() >= MAX_CONTEXT_FIELDS) {
reasonMask[0] |= REASON_MAX_CONTEXT_FIELDS;
state.reasonMask |= REASON_MAX_CONTEXT_FIELDS;
return;
}
if (key.length() > MAX_KEY_LENGTH) {
reasonMask[0] |= REASON_MAX_KEY_LENGTH;
state.reasonMask |= REASON_MAX_KEY_LENGTH;
return;
}
if (value == null || value.isNull()) {
out.put(key, null);
putPrunedValue(out, key, null, state);
return;
}
if (value.isString()) {
final String s = value.asString();
if (s.length() > MAX_VALUE_LENGTH) {
reasonMask[0] |= REASON_MAX_VALUE_LENGTH;
state.reasonMask |= REASON_MAX_VALUE_LENGTH;
return;
}
out.put(key, s);
putPrunedValue(out, key, s, state);
return;
}
if (value.isBoolean() || value.isNumber() || value.isInstant()) {
out.put(key, convertValue(value));
putPrunedValue(out, key, convertValue(value), state);
return;
}
if (value.isList()) {
final List<Value> list = value.asList();
if (depth >= MAX_SNAPSHOT_DEPTH) {
reasonMask[0] |= REASON_MAX_SNAPSHOT_DEPTH;
state.reasonMask |= REASON_MAX_SNAPSHOT_DEPTH;
return;
}
if (!seen.add(list)) {
reasonMask[0] |= REASON_CYCLE;
state.reasonMask |= REASON_CYCLE;
return;
}
if (list.size() > MAX_LIST_ELEMENTS) {
reasonMask[0] |= REASON_MAX_LIST_ELEMENTS;
state.reasonMask |= REASON_MAX_LIST_ELEMENTS;
}
final int limit = Math.min(list.size(), MAX_LIST_ELEMENTS);
for (int i = 0; i < limit; i++) {
if (out.size() >= MAX_CONTEXT_FIELDS) {
reasonMask[0] |= REASON_MAX_CONTEXT_FIELDS;
state.reasonMask |= REASON_MAX_CONTEXT_FIELDS;
break;
}
copyPrunedValue(out, key + "[" + i + "]", list.get(i), seen, depth + 1, reasonMask);
copyPrunedValue(out, key + "[" + i + "]", list.get(i), seen, depth + 1, state);
}
seen.remove(list);
return;
}
if (value.isStructure()) {
final Structure structure = value.asStructure();
if (depth >= MAX_SNAPSHOT_DEPTH) {
reasonMask[0] |= REASON_MAX_SNAPSHOT_DEPTH;
state.reasonMask |= REASON_MAX_SNAPSHOT_DEPTH;
return;
}
if (!seen.add(structure)) {
reasonMask[0] |= REASON_CYCLE;
state.reasonMask |= REASON_CYCLE;
return;
}
int walked = 0;
for (final String property : structure.keySet()) {
if (walked >= MAX_STRUCTURE_PROPERTIES) {
reasonMask[0] |= REASON_MAX_STRUCTURE_PROPERTIES;
state.reasonMask |= REASON_MAX_STRUCTURE_PROPERTIES;
break;
}
if (out.size() >= MAX_CONTEXT_FIELDS) {
reasonMask[0] |= REASON_MAX_CONTEXT_FIELDS;
state.reasonMask |= REASON_MAX_CONTEXT_FIELDS;
break;
}
walked++;
copyPrunedValue(
out, key + "." + property, structure.getValue(property), seen, depth + 1, reasonMask);
out, key + "." + property, structure.getValue(property), seen, depth + 1, state);
}
seen.remove(structure);
}
}

private static void putPrunedValue(
final Map<String, Object> out, final String key, final Object value, final CopyState state) {
state.estimatedRetainedBytes +=
FlagEvalEventMemoryEstimator.contextEntryRetainedBytes(key, value);
out.put(key, value);
}

private static final class CopyState {
private int reasonMask;
private long estimatedRetainedBytes;

private CopyState(final long estimatedRetainedBytes) {
this.estimatedRetainedBytes = estimatedRetainedBytes;
}
}

@FunctionalInterface
private interface NumberComparator {
boolean compare(double a, double b);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ public void finallyAfter(
// consulted by the aggregator, so skip the bounded copy entirely — the copy cost only
// applies under consent-on.
final Map<String, Object> attrs;
final long estimatedContextRetainedBytes;
if (observeFullEvaluationData && ctx != null && ctx.getCtx() != null) {
// Bounded copy of the caller's mutable context (see DDEvaluator.copyPrunedContext for
// every retained-size cap). Runs inline because the event is consumed asynchronously
Expand All @@ -147,8 +148,10 @@ public void finallyAfter(
w.countContextTruncated(copy.truncatedReason);
}
attrs = copy.attrs;
estimatedContextRetainedBytes = copy.estimatedRetainedBytes;
} else {
attrs = Collections.emptyMap();
estimatedContextRetainedBytes = 0;
}

w.enqueue(
Expand All @@ -160,7 +163,8 @@ public void finallyAfter(
errorMessage,
evalTimeMs,
observeFullEvaluationData,
attrs));
attrs,
estimatedContextRetainedBytes));
} catch (LinkageError e) {
// Never let EVP recording break flag evaluation
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import datadog.trace.api.featureflag.FeatureFlaggingGateway;
import datadog.trace.api.featureflag.flagevaluation.FlagEvalEventMemoryEstimator;
import datadog.trace.api.featureflag.ufc.v1.Allocation;
import datadog.trace.api.featureflag.ufc.v1.ConditionConfiguration;
import datadog.trace.api.featureflag.ufc.v1.ConditionOperator;
Expand Down Expand Up @@ -790,6 +791,9 @@ public void testCopyPrunedContextNoTruncationReturnsNullReason() {

assertThat(result.attrs, hasEntry("region", "us-east-1"));
assertThat(result.truncatedReason, equalTo(null));
assertThat(
result.estimatedRetainedBytes,
equalTo(FlagEvalEventMemoryEstimator.retainedContextBytes(result.attrs)));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,17 @@ void contextAttributesAreFlattenedAndConvertedInline() {
assertEquals(42, attrs.get("score"));
assertEquals("gold", attrs.get("profile.tier"));
assertFalse(attrs.containsKey("targetingKey"));
final FlagEvalEvent fallbackEstimate =
new FlagEvalEvent(
captured.get().flagKey,
captured.get().variant,
captured.get().allocationKey,
captured.get().targetingKey,
captured.get().errorMessage,
captured.get().evalTimeMs,
captured.get().observeFullEvaluationData,
attrs);
assertEquals(fallbackEstimate.estimatedRetainedBytes, captured.get().estimatedRetainedBytes);
assertTrue(
attrs.values().stream().noneMatch(Value.class::isInstance),
"context attrs must contain converted scalar values, not OpenFeature Value wrappers");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ public final class FlagEvalEvent {
*/
public final boolean observeFullEvaluationData;

/** Estimated retained bytes for this complete event while it waits in the queue. */
public final long estimatedRetainedBytes;

/** Convenience constructor; consent defaults to the privacy-preserving false. */
public FlagEvalEvent(
final String flagKey,
Expand All @@ -62,7 +65,16 @@ public FlagEvalEvent(
final String targetingKey,
final long evalTimeMs,
final Map<String, Object> attrs) {
this(flagKey, variant, allocationKey, targetingKey, null, evalTimeMs, false, attrs);
this(
flagKey,
variant,
allocationKey,
targetingKey,
null,
evalTimeMs,
false,
attrs,
FlagEvalEventMemoryEstimator.UNKNOWN_RETAINED_BYTES);
}

/** Convenience constructor; consent defaults to the privacy-preserving false. */
Expand All @@ -74,7 +86,16 @@ public FlagEvalEvent(
final String errorMessage,
final long evalTimeMs,
final Map<String, Object> attrs) {
this(flagKey, variant, allocationKey, targetingKey, errorMessage, evalTimeMs, false, attrs);
this(
flagKey,
variant,
allocationKey,
targetingKey,
errorMessage,
evalTimeMs,
false,
attrs,
FlagEvalEventMemoryEstimator.UNKNOWN_RETAINED_BYTES);
}

public FlagEvalEvent(
Expand All @@ -86,6 +107,28 @@ public FlagEvalEvent(
final long evalTimeMs,
final boolean observeFullEvaluationData,
final Map<String, Object> attrs) {
this(
flagKey,
variant,
allocationKey,
targetingKey,
errorMessage,
evalTimeMs,
observeFullEvaluationData,
attrs,
FlagEvalEventMemoryEstimator.UNKNOWN_RETAINED_BYTES);
}

public FlagEvalEvent(
final String flagKey,
final String variant,
final String allocationKey,
final String targetingKey,
final String errorMessage,
final long evalTimeMs,
final boolean observeFullEvaluationData,
final Map<String, Object> attrs,
final long estimatedContextRetainedBytes) {
this.flagKey = flagKey;
this.variant = variant;
this.allocationKey = allocationKey;
Expand All @@ -94,5 +137,7 @@ public FlagEvalEvent(
this.evalTimeMs = evalTimeMs;
this.observeFullEvaluationData = observeFullEvaluationData;
this.attrs = attrs != null ? attrs : Collections.emptyMap();
this.estimatedRetainedBytes =
FlagEvalEventMemoryEstimator.estimateRetainedBytes(this, estimatedContextRetainedBytes);
}
}
Loading
Loading