profiles: improve JFR export example and align LinkData null-element with spec - #8349
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #8349 +/- ##
=========================================
Coverage 91.27% 91.27%
Complexity 10472 10472
=========================================
Files 1006 1006
Lines 28277 28277
Branches 3569 3569
=========================================
Hits 25811 25811
Misses 1674 1674
Partials 792 792 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@open-telemetry/profiling-maintainers This PR is somewhat interesting, as one of the early examples of an end-to-end interop where the sender and receiver are written by different people working more or less in isolation from one another. It exposes some rough edges that may represent opportunities/requirements for spec changes to make it a smoother process.
|
zeitlinger
left a comment
There was a problem hiding this comment.
Nice metadata additions. Two suggestions:
Hoist hot-loop dictionary lookups. In JfrExecutionSampleEventConverter.accept() the "thread.name" key index and KeyValueAndUnitData are rebuilt for every sample event. Same in JfrLocationDataCompositor.frameToLocation() for "profile.frame.type"/"jvm" per frame. The dict dedupes so output is correct, but each call still allocates a string + KeyValueAndUnitData. Since the key/value pair is constant per converter, compute it once (e.g. in the constructor or lazily cached) and reuse the int index.
For the thread sample, only threadName/threadNameData vary — pre-compute the "thread.name" key index once.
Null sampledThread. recordedEvent.getValue("sampledThread") can be null for some ExecutionSample variants. A null guard (skip or fall back to "unknown") would harden the converter against truncated/synthetic events.
LGTM otherwise — the ValueTypeData fix and frame-type attribute look right.
9032cb5 to
5e09b77
Compare
|
Hi Gregor Thanks for taking a look.
Right. There are two subtly different cases here, where one KV is entirely constant and the other is dependent on the event's thread value. That's partly an artifact of an over-simplification I made to assume all frames are "jvm" type. If native code is involved then the value there also becomes event-dependent. Nevertheless there is still a performance argument for caching, since the number of thread names / thread types is considerably smaller than the number of events, but it's a classic space/time tradeoff to add a HashMap for these and at this early stage I'm lacking data to support it. The thread name case in particularly is concerning, as it's an unbounded key space and thus unbounded cache size. I'm going with 'the cost of churning a short lived key object is tolerable', especially since the frame's nameFrom computation is a worse example of the same issue and will likely dominate either of the others.
Can it? The asserts in OpenJDK's jfrThreadSampling.cpp seemed to indicate it's always set, but ok, no real downside to hedging anyhow. I think the main takeaway here is the testing thus far is just a handful of old JFR files I had lying around and they don't contain some obvious alternative cases - the frame name code will break on non-java frames I think, but there aren't any in the test set... The JFR event APIs make it next to impossible to mock JFR data cleanly, which is a colossal pain for testing. OpenJDK itself seems to do it by having a curated collection of (hand crafted?) JFR files in version control instead. |
Pull request dashboard statusMerged · refreshed 2026-08-24 21:11 UTC Status above doesn't look right?
|
jack-berg
left a comment
There was a problem hiding this comment.
Hey sorry it took me so long to get involved in this.
I have a better system now for cutting through the endless noise of github. If you see I miss something like this, please ping me. 🙂
| linkTable.putIfAbsent(LinkData.create("", "")); | ||
| // TODO this is, strictly speaking, probably not profile spec compliant at present. | ||
| // The spec uses "" but the Id encoders don't like that. The alpha spec may need revision... | ||
| linkTable.putIfAbsent(LinkData.create(TraceId.getInvalid(), SpanId.getInvalid())); |
There was a problem hiding this comment.
PR title advertises an example-only change, but this modifies the shared SDK dictionary null-element semantics. Worth calling out in the PR description.
| linkTable.putIfAbsent(LinkData.create("", "")); | ||
| // TODO this is, strictly speaking, probably not profile spec compliant at present. | ||
| // The spec uses "" but the Id encoders don't like that. The alpha spec may need revision... | ||
| linkTable.putIfAbsent(LinkData.create(TraceId.getInvalid(), SpanId.getInvalid())); |
There was a problem hiding this comment.
Nothing in this PR appears to reference linkTable[0]. What specifically breaks with "", and where? Ideally the fix belongs in the id encoder (accept "" as invalid) rather than inflating the placeholder here.
There was a problem hiding this comment.
As discussed above the profiles draft spec inadvertently failed to align well with the trace spec on this. We tweaked the profile spec for better compatibility. It's up to you if you want to make the encoders more liberal - happy to generate a PR for that, but I'd lean towards not - encoding should be strict, decoding more flexible. Either way I'll edit out the now obsolete TODO source comment here, since the change is now explicitly spec compliant.
| } | ||
|
|
||
| String threadName = recordedThread.getJavaName(); | ||
| int threadNameIndex = profilesDictionaryCompositor.putIfAbsent("thread.name"); |
There was a problem hiding this comment.
Narrowing zeitlinger's suggestion: even without a per-thread cache, the "thread.name" key index itself is constant. Compute once in the constructor and store as an int field. No unbounded cache concern, and it drops one HashMap lookup + string allocation per event.
There was a problem hiding this comment.
If you keep pushing I'll probably just shrug and do it because I'm not heavily invested here, but the existing design wasn't an oversight:
-
There is no evidence the performance gain is worthwhile. Yes, it's a repeated lookup. I don't think it's a repeated allocation - the string is from the constant pool? I'm all in favour of efficient production code, but it's in tension with clean, easy to understand 'getting started' type examples. I was aiming at 'this is the simplest thing that can possibly work, you may use it as a starting point' for this, whilst you seem to be leaning a little more towards 'this is an example of how to do it efficiently, it's marginally harder to follow than the code for just doing it' i.e. more of an intermediate example than a beginner example?
-
Inserting it to the dictionary eagerly, such as in the constructor, is subtly incorrect. The dictionary lookup tables should not contain orphan entries, i.e. things that are not references from anywhere - what a managed heap would call garbage. In the case where the JFR file being processed contains no events that match to get encoded, the "thread.name" string would be such an orphan. It's possible to modify the profile encoding to check for orphans at the point just before serialization, but it's a computationally expensive check, so we're better off trying not to create orphans in the first place. This can be worked around by lazily inserting it the first time instead, so it's not a big problem.
There was a problem hiding this comment.
I'm not invested either. Let's just keep it as is. The stakes are low.
|
|
||
| LocationData locationData = | ||
| LocationData.create(0, 0, List.of(lineData), Collections.emptyList()); | ||
| LocationData.create(0, 0, List.of(lineData), List.of(typeAttributeIndex)); |
There was a problem hiding this comment.
If the frame-type KV is hoisted to a field, hoist List.of(typeAttributeIndex) alongside it. Same applies to List.of(attribIndex) in JfrExecutionSampleEventConverter once the attribute index is stable per thread.
5e09b77 to
4989ec7
Compare
Add metadata to the OTLP message so as to make it more interpretable by receiving backends.