[improve][test] Record Netty's allocator events and other JFR settings per profiled component, and profile on Alpine - #26787
Merged
lhotari merged 4 commits intoSep 30, 2026
Conversation
…ce runs, merge JFR configurations per component and profile on Alpine Profiled JVMs record JDK Flight Recorder's events with the merge of their component's jfrConfigurations, by default [profile, pulsar.jfc]: a name is one of the JDK's configurations, and a name ending with .jfc a file of tests/performance/jfr. Before the cluster starts, the launcher merges them with "jfr configure" in a one-off container of the component's image, so that the JDK's configurations are those of the JVM that records with them, into jfr-configuration.jfc beside the component's recordings, and passes it to async-profiler's jfrsync option; scenarios no longer set jfrsync, and the launcher rejects options that do. When the image's JDK can't merge them, the component records with the JDK's profile configuration. pulsar.jfc adds Netty's allocator chunk and reallocation events (io.netty.*). netty-allocations.jfc adds the events of every buffer allocation and free, which are costly, since there is an event for every buffer and JFR can't sample them (a broker recording grew from 10 MB to 1 GB in the max-rate scenario); configs/profile-<component>-netty-allocations profile a component with them. After a profiled run, NettyAllocatorEvents summarizes each measurement recording's Netty allocator events into <recording>.measurement.netty-allocator.json: events per second and per message, buffer and chunk allocations by allocator, memory and pooled or one-off chunk, buffer sizes, reallocations and allocating thread pools. The profile report shows it, and the summarizeNettyAllocatorEvents task summarizes other recordings. Profiled runs use the Alpine test image by default, as the unprofiled runs and Pulsar's default image do, since a profile of the Wolfi image, such as of its libc's memory allocation, doesn't carry over to them; -Pinttest.testImageVariant=wolfi profiles on the glibc-based image. The Alpine test image installs musl's debug symbols (musl-dbg), which Alpine strips from musl: without them, 44 % of a broker's CPU samples ended in an unnamed /lib/ld-musl-x86_64.so.1 frame, and the JNI frames above them were missing; with them, stacks read, for example, Socket.recvAddress, netty_unix_socket_recvAddress, recvfrom. The launcher also keeps launcher.log, with the containers' logs, when the applications received duplicates, ordering violations or invalid messages: such a run counted as successful and deleted it, which hid a broker that ran out of direct memory and restarted mid-run. Assisted-by: Claude Code (claude-opus-5-5)
dao-jun
approved these changes
Sep 30, 2026
…ents, allow profiling without JFR's events - jfrConfigurations defaults to [profile], and an empty list records without jfrsync, only async-profiler's events. A single configuration of the JDK is passed to jfrsync as it is, without merging. - netty-allocations.jfc has all of Netty's allocator events, enabled; pulsar.jfc is removed. - nettyAllocationsReport: true summarizes a component's Netty allocator events after the run, so that other runs don't scan their recordings for them. A recording with only some of the events, or none, gives a summary of those it has. - configs/profile-<component>-netty-allocations set both settings. - The docs, AGENTS.md and the configurations' comments describe the settings consistently: when jfrsync is added, the heavy overhead of the buffer events, that Netty 4.1, which Pulsar 4.x uses, has no allocator events, and that asyncProfilerOptions is a comma-separated list of async-profiler's options, as the "Launch as agent" column of its ProfilerOptions.md names them. Assisted-by: Claude Code (claude-opus-5-5)
Assisted-by: Claude Code (claude-opus-5-5)
…nfigurations
A profiled component's jfrEventConfig lists JFR events to enable, or event settings, each with event and optionally
setting and value, such as {event: jdk.CPULoad, setting: period, value: 100 ms}. The launcher applies them with
"jfr configure" after merging the component's jfrConfigurations into the jfr-configuration.jfc that it passes to
async-profiler's jfrsync option. With jfrConfigurations: [], they apply to the JDK's default configuration, as
"jfr configure" starts from without --input, and jfrConfigurations: [none] starts from an empty one. Without event
settings, a single configuration of the JDK still goes to jfrsync as it is, and [] or [none] leave jfrsync out.
Assisted-by: Claude Code (claude-opus-5-5)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
The performance tests' profiled runs (
tests/performance) record JDK Flight Recorder's events alongside async-profiler, with the JDK'sprofileconfiguration, which each scenario set with async-profiler'sjfrsyncoption. They had no way to record other JFR events, such as those of Netty's buffer allocators: Netty 4.2'sAdaptivePoolingAllocatorandPooledByteBufAllocatoremit JFR events for their buffer and chunk allocations (io.netty.*, see Netty'smicrobench/src/main/resources/Netty Allocator Events.jfc). With them, a profile shows how much memory the allocators allocate and free, which buffers don't fit the pooled memory, and how often buffers grow. For example, in the IoT telemetry max-rate scenario the broker's allocator allocated and freed about 660 chunks per second, about 94 MB/s, at a steady load, which led to #26786.Adding them brought up three more problems with the profiling setup:
/lib/ld-musl-x86_64.so.1, and the JNI frames above it were missing. That was 44 % of a broker's CPU samples in the max-rate scenario.launcher.log, which has the containers' logs. A broker that ran out of direct memory and restarted mid-run left no trace of why.Modifications
JFR configurations per component. A profiled component's settings take
jfrConfigurations, a list of JFR configurations that the launcher merges with the JDK'sjfr configure. The default is[profile].profileordefault, is one of the JDK's configurations..jfcis a file of the newtests/performance/jfrdirectory.noneis an empty configuration.JFR event settings per component.
jfrEventConfiglists JFR events to enable, or event settings, each witheventand optionallysettingandvalue, such as{event: jdk.CPULoad, setting: period, value: 100 ms}. They apply after the configurations. WithjfrConfigurations: []they apply to the JDK'sdefaultconfiguration, asjfr configuredoes without--input, and withjfrConfigurations: [none]to an empty one.How the launcher sets
jfrsync.jfrEventConfig, such as the defaultprofile, goes to async-profiler'sjfrsyncoption as it is.jfr configurein a one-off container of the component's image, so that the JDK's configurations are those of the JVM that records with them. It writes the merge intojfr-configuration.jfcbeside the component's recordings and passes that file tojfrsync.jfrConfigurations: []or[none]withoutjfrEventConfigleavesjfrsyncout, so the component records only async-profiler's samples.jfrsync, and the launcher rejects options that do.jfrtool, the component records with the JDK'sprofileconfiguration, and the launcher says so.Netty's allocator events.
netty-allocations.jfcenables all of them:io.netty.AllocateBuffer,ReallocateBuffer,FreeBuffer,AllocateChunk,FreeChunkandReturnChunk.throttlesetting doesn't apply to events without@Throttle. In the max-rate scenario there were about 2.5 buffer events of each kind per message in each profiled component, and a broker recording of 1 GB instead of 10 MB.ReturnChunkisn't in a Netty release yet. Netty 4.1, which Pulsar 4.x uses, has none, so profiling a Pulsar 4.x image records none.The Netty allocations report. A component's
nettyAllocationsReport: truemakes the launcher summarize its measurement recording's Netty allocator events after the run into<recording>.measurement.netty-allocator.json, which the profile report shows in a "Netty allocator events" section:The report is separate from recording the events, so other runs don't scan their recordings. A recording with only some of the events, or none, gives a summary of those it has. The
summarizeNettyAllocatorEventstask summarizes other recordings.Netty allocation profiles.
configs/profile-{broker,gateways,applications}-netty-allocations.yamlsetjfrConfigurations: [profile, netty-allocations.jfc]andnettyAllocationsReport: truefor a component, in place ofconfigs/profile-<component>.Profiling on Alpine.
-Pinttest.testImageVariant=wolfistill profiles on the Wolfi image.musl-dbg. apk pins it to the installed musl version, and it isn't installed on the Wolfi image. With them, a stack reads, for example,Socket.recvAddress→netty_unix_socket_recvAddress→recvfrom→__syscall_cp_c. Pulsar's own Docker image is unchanged.Keeping the logs of incorrect deliveries. The launcher keeps
launcher.logwhen the applications received duplicates, ordering violations or invalid messages.Documentation.
docs/profiling.md("The JFR configuration", "Netty allocator events") anddocs/analyzing-profiles.md("Netty allocator events").AGENTS.md, including the overhead of profiling Netty's allocations.asyncProfilerOptionsis a comma-separated list of async-profiler's options, as the "Launch as agent" column of its ProfilerOptions.md names them.Verifying this change
This change added tests and can be verified as follows:
ProfilingSettingsTestcoversjfrConfigurations,jfrEventConfigandnettyAllocationsReport, including thejfr configurecommand line,defaultandnone,[]and[none]turning JFR off, and rejecting invalid settings and ajfrsyncin the options.NettyAllocatorEventsTestrecords stand-in events with Netty's event names and fields, and checks the summary and the report section, also for a recording without them.DeliveryCheckTestcovers keepinglauncher.log.jfr configurechecks in the test image. Mergingprofilewithnetty-allocations.jfc, and applying event settings such as+jdk.CPULoad#period=100 msafter it, gives the expected effective settings, which override those of the configurations.netty-allocations.jfcandnettyAllocationsReport: true. The broker recorded withjfrsync=profile, with no merge and no Netty summary. The applications recorded with the mergedjfr-configuration.jfc, and their profile report's Netty section shows their buffer events, about 0.05 buffer allocations per message. Every message was delivered.musl-dbg: CPU samples with an unnamedld-muslframe went from 44.3 % (2,600 of 5,869) to none (0 of 5,798).jfrEventConfigwas verified with the unit tests and thejfr configurechecks, not in a profiled run.Does this pull request potentially affect one of the following parts:
If the box was checked, please highlight the changes
This change was prepared with the assistance of Claude Code (claude-opus-5-5); I have reviewed and verified it.