|
| 1 | +/* |
| 2 | + * Copyright 2026, Datadog, Inc |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | +package com.datadoghq.profiler; |
| 17 | + |
| 18 | +import java.nio.charset.StandardCharsets; |
| 19 | +import java.util.concurrent.atomic.AtomicReferenceArray; |
| 20 | + |
| 21 | +/** |
| 22 | + * Process-wide cache resolving an attribute value to its {@code (encoding, utf8)} pair for the |
| 23 | + * all-native context write path, avoiding a JNI {@code registerConstant0} call on every write. |
| 24 | + * |
| 25 | + * <p>Within a single recording session the encoding is a <em>stable</em> ID: {@code registerConstant0} |
| 26 | + * interns the value in the native Dictionary and returns the same ID for the life of that session. |
| 27 | + * That stability is what makes a single shared, lock-free cache correct without per-thread copies: |
| 28 | + * <ul> |
| 29 | + * <li>Entries are immutable and published atomically (one array slot store), so a concurrent |
| 30 | + * reader never sees a torn {@code (key, encoding, utf8)}.</li> |
| 31 | + * <li>A miss just calls {@code registerConstant0} again — idempotent, returns the same encoding — |
| 32 | + * and re-stores an equivalent entry; racing writers converge (last write wins, all correct).</li> |
| 33 | + * <li>Direct-mapped by {@code value.hashCode()}; a hash collision evicts the previous value, which |
| 34 | + * is simply re-resolved on its next use.</li> |
| 35 | + * </ul> |
| 36 | + * |
| 37 | + * <p><b>Session boundary:</b> the encoding is stable only <em>within</em> a recording session. A |
| 38 | + * fresh {@code start} (native {@code Profiler::start} with reset) calls |
| 39 | + * {@code StringDictionary::clearAll()}, which resets the ID counter, so encodings from the previous |
| 40 | + * session become stale. {@link JavaProfiler} therefore calls {@link #clear()} on every {@code start} |
| 41 | + * command; a subsequent {@link #resolve} re-registers the value and re-caches its new encoding. |
| 42 | + * |
| 43 | + * <p>Replaces the per-{@link ThreadContext} value cache: in the all-native model there is no |
| 44 | + * per-thread {@code ThreadContext} instance to host it, and a global cache avoids duplicating the |
| 45 | + * table across every carrier / virtual thread. |
| 46 | + */ |
| 47 | +final class ContextValueCache { |
| 48 | + |
| 49 | + /** Max UTF-8 byte length for a value (the OTEP attrs_data entry length field is one byte). */ |
| 50 | + static final int MAX_VALUE_BYTES = 255; |
| 51 | + |
| 52 | + private static final int SIZE = 256; |
| 53 | + private static final int MASK = SIZE - 1; |
| 54 | + |
| 55 | + /** Immutable resolved value; published as a single atomic array-slot store. */ |
| 56 | + static final class Entry { |
| 57 | + final String key; |
| 58 | + final int encoding; |
| 59 | + final byte[] utf8; |
| 60 | + |
| 61 | + Entry(String key, int encoding, byte[] utf8) { |
| 62 | + this.key = key; |
| 63 | + this.encoding = encoding; |
| 64 | + this.utf8 = utf8; |
| 65 | + } |
| 66 | + } |
| 67 | + |
| 68 | + private final AtomicReferenceArray<Entry> table = new AtomicReferenceArray<>(SIZE); |
| 69 | + |
| 70 | + /** |
| 71 | + * Resolves {@code value} to its cached {@code (encoding, utf8)} pair, registering it on a miss. |
| 72 | + * |
| 73 | + * @return the entry, or {@code null} if the value cannot be represented — {@code null} input, a |
| 74 | + * UTF-8 encoding longer than {@value #MAX_VALUE_BYTES} bytes, or a full native Dictionary |
| 75 | + * (encoding {@code < 0}). Callers treat {@code null} as "no attribute" (skip / clear). |
| 76 | + */ |
| 77 | + Entry resolve(String value) { |
| 78 | + if (value == null) { |
| 79 | + return null; |
| 80 | + } |
| 81 | + int slot = value.hashCode() & MASK; |
| 82 | + Entry e = table.get(slot); |
| 83 | + if (e != null && value.equals(e.key)) { |
| 84 | + return e; // hit — no JNI |
| 85 | + } |
| 86 | + // Miss: encode + validate size before touching the Dictionary (a rejected value must not |
| 87 | + // create an orphan Dictionary entry). |
| 88 | + byte[] utf8 = value.getBytes(StandardCharsets.UTF_8); |
| 89 | + if (utf8.length > MAX_VALUE_BYTES) { |
| 90 | + return null; |
| 91 | + } |
| 92 | + int encoding = ThreadContext.registerConstant0(value); |
| 93 | + if (encoding < 0) { |
| 94 | + return null; // Dictionary full |
| 95 | + } |
| 96 | + Entry ne = new Entry(value, encoding, utf8); |
| 97 | + table.set(slot, ne); // benign race: converges on an equivalent entry |
| 98 | + return ne; |
| 99 | + } |
| 100 | + |
| 101 | + /** |
| 102 | + * Drops all cached entries. Called when a fresh recording session starts and the native |
| 103 | + * Dictionary is reset, so stale encodings from the previous session are not reused. A concurrent |
| 104 | + * {@link #resolve} racing this simply observes a miss and re-registers the value against the new |
| 105 | + * Dictionary — correct, just an extra {@code registerConstant0} call. |
| 106 | + */ |
| 107 | + void clear() { |
| 108 | + for (int i = 0; i < SIZE; i++) { |
| 109 | + table.set(i, null); |
| 110 | + } |
| 111 | + } |
| 112 | +} |
0 commit comments