Skip to content

profiles: improve JFR export example and align LinkData null-element with spec - #8349

Merged
jack-berg merged 3 commits into
open-telemetry:mainfrom
jhalliday:jh-profiling-t
Aug 24, 2026
Merged

profiles: improve JFR export example and align LinkData null-element with spec#8349
jack-berg merged 3 commits into
open-telemetry:mainfrom
jhalliday:jh-profiling-t

Conversation

@jhalliday

Copy link
Copy Markdown
Contributor

Add metadata to the OTLP message so as to make it more interpretable by receiving backends.

@jhalliday
jhalliday requested a review from a team as a code owner April 30, 2026 13:18
@codecov

codecov Bot commented Apr 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.27%. Comparing base (35636ae) to head (4989ec7).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jhalliday

Copy link
Copy Markdown
Contributor Author

@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.

  • Some metadata is required by the backend, but not by the spec. On the one had the backend (devfiler) is behaving reasonably - it can't interpret data and render visualizations without it. On the other hand, unless I missed something it's not specified or required by the spec at present. "thread.name", for example. Opportunity to add 'SHOULD'-level recommendations to the semconv?
  • The 'null' dict_table[0] element is defined as a wire-level empty value to minimize wire bytes, but in the case of Link the trace spec already defined that a) the span/trace are of fixed, non-zero length and b) specifically the invalid value of each is (in string form) "0000...". These requirements are contradictory. I think the profiling wire spec should go with the trace spec definition, despite the extra bytes. In practice it will compress anyhow and the hassle of retrofitting trace codecs to deal with zero length strings does not appeal. Here for example the existing Java SDK will throw if "" is used and it's hard to call that wrong, so code duplication or config flags are the only available routes to evolve it.

@zeitlinger zeitlinger left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

@jhalliday

Copy link
Copy Markdown
Contributor Author

Hi Gregor

Thanks for taking a look.

Hoist hot-loop dictionary lookups.

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.

Null sampledThread. recordedEvent.getValue("sampledThread") can be null for some ExecutionSample variants.

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.

@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Jul 18, 2026

Copy link
Copy Markdown

Pull request dashboard status

Merged · refreshed 2026-08-24 21:11 UTC

Status above doesn't look right?
  • Anything look wrong? Report it with what you expected; it helps us improve the dashboard.

@jack-berg jack-berg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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()));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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()));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

@jack-berg jack-berg changed the title profiles: improve JFR export example profiles: improve JFR export example and align LinkData null-element with spec Aug 24, 2026
@jack-berg
jack-berg merged commit 16f13a0 into open-telemetry:main Aug 24, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants