Skip to content

[improve][broker] Improve the performance of TopicName constructor#24463

Merged
lhotari merged 1 commit intoapache:masterfrom
BewareMyPower:bewaremypower/improve-topic-name-parse
Apr 21, 2026
Merged

[improve][broker] Improve the performance of TopicName constructor#24463
lhotari merged 1 commit intoapache:masterfrom
BewareMyPower:bewaremypower/improve-topic-name-parse

Conversation

@BewareMyPower
Copy link
Copy Markdown
Contributor

@BewareMyPower BewareMyPower commented Jun 25, 2025

Motivation

The TopicName's constructor has poor performance:

  • NamespaceName#get is very slow
  • Splitter.on("/").splitToList(rest) is slow
  • String.format is slower than the + operation on strings and completeTopicName is unnecessarily created again

Modifications

  • Initialize NamespaceName in a lazy way (don't do that because it assumes the constructor is called more frequently than getNamespace or getNamespaceObject)
  • Replace Splitter with a manually written splitBySlash introduced from [fix][proxy] Fix proxy OOM by replacing TopicName with a simple conversion method #24465. Actually, StringUtils#split has good performance as well. But it will split "//xxx/yyy/zzz" to ["xxx", "yyy", "zzz"] without reporting any error.
  • Initialize completeTopicName from the argument directly without any concentrate operation
  • Replace String.format with + in fromPersistenceNamingEncoding

NamespaceName and fromPersistenceNamingEncoding will not be handled in this PR.

Documentation

  • doc
  • doc-required
  • doc-not-needed
  • doc-complete

Matching PR in forked repository

PR in forked repository: BewareMyPower#44

@BewareMyPower BewareMyPower self-assigned this Jun 25, 2025
@github-actions github-actions Bot added the doc-not-needed Your PR changes do not impact docs label Jun 25, 2025
@BewareMyPower
Copy link
Copy Markdown
Contributor Author

Before 48ffb7a, there is a constructor parameter that determines whether to initialize the NamespaceName. I also compared with the existing TopicName constructor, which can be found here: https://gist.github.com/BewareMyPower/6b83c897552c110f336e51965cc91c24

The benchmark result is:

TopicNameBenchmark.testConstruct                          thrpt       312.983          ops/s
TopicNameBenchmark.testConstructWithNamespaceInitialized  thrpt       101.973          ops/s
TopicNameBenchmark.testLegacyTopicName                    thrpt        53.944          ops/s
TopicNameBenchmark.testReadFromCache                      thrpt       721.392          ops/s

As you can see

  • Accessing it from the cache is only 2.3x faster than the latest implementation, which is not significant. However, to avoid debate on whether to remove cache in this PR, I only exposed the public constructor.
  • Skipping initialization of NamespaceName is 3x faster than initializing it immediately
  • It's 6x faster than the original implementation. Even if initializing NamespaceName immediately, it's still about 2x faster.

@lhotari
Copy link
Copy Markdown
Member

lhotari commented Jun 25, 2025

Before 48ffb7a, there is a constructor parameter that determines whether to initialize the NamespaceName. I also compared with the existing TopicName constructor, which can be found here: https://gist.github.com/BewareMyPower/6b83c897552c110f336e51965cc91c24

The benchmark result is:

TopicNameBenchmark.testConstruct                          thrpt       312.983          ops/s
TopicNameBenchmark.testConstructWithNamespaceInitialized  thrpt       101.973          ops/s
TopicNameBenchmark.testLegacyTopicName                    thrpt        53.944          ops/s
TopicNameBenchmark.testReadFromCache                      thrpt       721.392          ops/s

As you can see

  • Accessing it from the cache is only 2.3x faster than the latest implementation, which is not significant. However, to avoid debate on whether to remove cache in this PR, I only exposed the public constructor.
  • Skipping initialization of NamespaceName is 3x faster than initializing it immediately
  • It's 6x faster than the original implementation. Even if initializing NamespaceName immediately, it's still about 2x faster.

I wouldn't trust JMH results from benchmarking on Mac OS. In the case of PR #24457, the results are very different when benchmarking on Linux x86_64 with Intel i9 processor (Dell XPS 2019 laptop).
If we'd completely remove the cache, there would be more duplicate TopicName and NamespaceName instances and more duplicate string instance. For very long tenant, namespace and topic names, that could add up a significant amount of wasted heap memory.

@BewareMyPower
Copy link
Copy Markdown
Contributor Author

@lhotari So could you help run benchmark in your Linux environment to see the difference? I can also create a workflow via GitHub Actions to see the result.

@BewareMyPower
Copy link
Copy Markdown
Contributor Author

If we'd completely remove the cache, there would be more duplicate TopicName and NamespaceName instances and more duplicate string instance. For very long tenant, namespace and topic names, that could add up a significant amount of wasted heap memory.

I don't want to debate about it in this PR. If you have a chance to look at how TopicName is used in Pulsar, you will find a lot of temporary TopicName instances just for conversion. I know near all Pulsar constributors are very sensitive to the topic about GC, so I don't change the TopicName#get implementation.

The ultimate solution is to create some utils methods instead of leveraging the TopicName class.

@BewareMyPower
Copy link
Copy Markdown
Contributor Author

Updated test results to compare it with the legacy implementation in https://github.com/BewareMyPower/pulsar/actions/runs/15872434081/job/44752001433?pr=45

TopicNameBenchmark.testConstruct                             thrpt       258.932          ops/s
TopicNameBenchmark.testConstructWithNamespaceInitialization  thrpt       158.876          ops/s
TopicNameBenchmark.testLegacyConstruct                       thrpt        44.290          ops/s
TopicNameBenchmark.testReadFromCache                         thrpt       340.613          ops/s
  • The constructor is 5.8x faster than the existing one
  • The constructor with NamespaceName initialization is 3.6x faster than the existing one

@BewareMyPower
Copy link
Copy Markdown
Contributor Author

@lhotari @codelipenghui @nodece @dao-jun @poorbarcode @coderzc This PR is now ready to review, PTAL

@BewareMyPower BewareMyPower changed the title [improve][common] Improve the performance of TopicName [improve][broker] Improve the performance of TopicName Jul 14, 2025
Comment thread pulsar-common/src/main/java/org/apache/pulsar/common/naming/TopicName.java Outdated
Comment thread pulsar-common/src/main/java/org/apache/pulsar/common/naming/TopicName.java Outdated
Comment thread microbench/src/main/java/org/apache/pulsar/broker/qos/TopicNameBenchmark.java Outdated
@lhotari
Copy link
Copy Markdown
Member

lhotari commented Jul 25, 2025

@BewareMyPower My point from an earlier comment isn't addressed:

If we'd completely remove the cache, there would be more duplicate TopicName and NamespaceName instances and more duplicate string instance. For very long tenant, namespace and topic names, that could add up a significant amount of wasted heap memory.

In this case, I think it's irrelevant to just benchmark the performance of creating/looking up TopicName instances.

Duplicate java.lang.String instances could increase significantly after introducing a non-caching solution. Heapdump analysis tools have such checks for duplicate instances so that's one way how to compare the difference.

I do agree that a lot of the TopicName handling code is a mess. For example in Topic listing, the topic name is converted from/to String multiple times. That's adding up a lot of pressure for a fast lookup solution when instances are cached. So I'm not against changing the current caching, it's just that there's a need to consider duplicate instances as well. It's likely that the caching is more relevant for NamespaceName than TopicName regarding duplicate instances. We might not be keeping a reference to TopicName instance in that many places when broker is running.

@BewareMyPower
Copy link
Copy Markdown
Contributor Author

If we'd completely remove the cache,

I've read more code and changed my idea a bit. Caching could be helpful in many cases. But how to establish the cache might depend on specific use cases. Writing a common cache is hard. I don't like the solution in #23052, but it's anyway good to resolve the issue encountered at that time.

As I've mentioned here, exposing the public method is helpful for downstream to construct its own cache. In addition, the TopicName can be cached as well as some other classes, e.g. Map<String, Pair<TopicName, PersistentTopic>> would be better than maintaining two maps:

  • A built-in Map<String, TopicName>
  • An external Map<String, PersistentTopic>

It's just an example, PersistentTopic can be replaced by another topic abstraction class.

@BewareMyPower
Copy link
Copy Markdown
Contributor Author

Regarding this PR, I'm going to revert other changes and only leaving the improvement on TopicName's constructor itself.

Exposing a public method will be easier for the downstream to maintain its custom cache, but it will also be confusing for the core Pulsar developers to make the decision on TopicName.get and new TopicName.

Anyway, we should improve the use of TopicName case by case. Take BrokerService for example, a TopicName is passed to getTopic, but the string returned by toString() is passed to

  • loadOrCreatePersistentTopic
  • TopicLoadingContext#topic

The TopicName instance is constructed again by them to checkOwnershipAndCreatePersistentTopic.

In this case, TopicName#get makes sense than new TopicName. But the code is still inefficient, we should not perform the unnecessary topicName.toString() -> TopicName.get(topic) conversions.

@lhotari
Copy link
Copy Markdown
Member

lhotari commented Jul 25, 2025

This PR will be an improvement. Regarding the argument I made in a previous comment about duplicate tenant and namespace java.lang.String instances, that problem is already present in the current code base. That could be solved in a different PR.

Copy link
Copy Markdown
Member

@lhotari lhotari left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Copy Markdown
Member

@lhotari lhotari left a comment

Choose a reason for hiding this comment

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

For deduplicating the tenant and namespace String instances, it would be useful to assign the tenant and namespacePortion fields from the NamespaceName instance. This wouldn't add much overhead, but benefit in reducing the amount of heap memory since there would be less duplication of java.lang.String instances at runtime.

@BewareMyPower BewareMyPower force-pushed the bewaremypower/improve-topic-name-parse branch from e147446 to 4755500 Compare April 21, 2026 07:48
@BewareMyPower
Copy link
Copy Markdown
Contributor Author

@lhotari Do you have a chance to review this PR again according to #25367 (comment)?

I've minimize the change of this PR to only optimize the constructor of the TopicName. It would save the CPU time when the cache is missed. NamespaceName and TopicName#fromPersistenceNamingEncoding won't be handled in this PR.

@BewareMyPower BewareMyPower changed the title [improve][broker] Improve the performance of TopicName [improve][broker] Improve the performance of TopicName constructor Apr 21, 2026
@BewareMyPower
Copy link
Copy Markdown
Contributor Author

Updated benchmark can be found here: https://github.com/BewareMyPower/JavaBenchmark/actions/runs/24712104796/job/72279087737

TopicNameBenchmark.testConstruct                                  thrpt             101.457                 ops/s
TopicNameBenchmark.testLegacyConstruct                            thrpt              36.061                 ops/s

my local run:

TopicNameBenchmark.testConstruct                                  thrpt             133.459                ops/s
TopicNameBenchmark.testLegacyConstruct                            thrpt              53.429                ops/s

It's 2.5x ~ 2.8x faster.

Actually, the performance is also affected by the slow NamespaceName#get call. I tried removing the namespace field from both LegacyTopicName and TopicName and ran the benchmark again, the result is:

Benchmark                                Mode  Cnt    Score   Error  Units
TopicNameBenchmark.testConstruct        thrpt       526.592          ops/s
TopicNameBenchmark.testLegacyConstruct  thrpt        73.782          ops/s

It's 7x faster

Copy link
Copy Markdown
Member

@lhotari lhotari left a comment

Choose a reason for hiding this comment

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

LGTM, great work @BewareMyPower

@lhotari lhotari merged commit 3130a93 into apache:master Apr 21, 2026
44 of 45 checks passed
lhotari pushed a commit that referenced this pull request Apr 21, 2026
lhotari pushed a commit that referenced this pull request Apr 21, 2026
@lhotari
Copy link
Copy Markdown
Member

lhotari commented Apr 21, 2026

I backported this to maintenance branches too.

@lhotari
Copy link
Copy Markdown
Member

lhotari commented Apr 21, 2026

Actually, the performance is also affected by the slow NamespaceName#get call. I tried removing the namespace field from both LegacyTopicName and TopicName and ran the benchmark again, the result is:

One additional detail to check is to see if optimizing org.apache.pulsar.common.naming.TopicDomain#getEnum would help. A general performance advice has been in the past to avoid calling Enum's values() in hot paths.

something like this:

    public static TopicDomain getEnum(String value) {
        if (persistent.value.equalsIgnoreCase(value)) {
           return persistent;
        }
        if (non_persistent.value.equalsIgnoreCase(value)) {
           return non_persistent;
        }
        if (topic.value.equalsIgnoreCase(value)) {
           return topic;
        }
        if (segment.value.equalsIgnoreCase(value)) {
           return segment;
        }
        throw new IllegalArgumentException("Invalid topic domain: '" + value + "'");
    }

I'm not sure if this type of optimization applies to modern JVMs.

lhotari pushed a commit that referenced this pull request Apr 21, 2026
priyanshu-ctds pushed a commit to datastax/pulsar that referenced this pull request Apr 22, 2026
srinath-ctds pushed a commit to datastax/pulsar that referenced this pull request Apr 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants