Skip to content

Conversation

arvi18
Copy link

@arvi18 arvi18 commented Apr 26, 2025

In order to better analyze steady-state performance of Kafka, this PR enables a warmup in the Producer Performance test. The warmup duration is specified as a number of records that are a subset of the total numRecords. If warmup records is greater than 0, the warmup is represented by a second Stats object which holds warmup results. Once warmup records have been exhausted, the test switches to using the existing Stats object. At end of test, if warmup was enabled, the summary of the whole test (warump + steady state) is printed followed by the summary of the steady-state portion of the test. If no warmup is used, summary prints don't change from existing behavior.
This contribution is an original work and is licensed to the Kafka project under the Apache license

Testing strategy comprises new Java unit tests added to ProducerPerformanceTests.java.

Committer Checklist (excluded from commit message)

  • Verify design and implementation
  • Verify test coverage and CI build status
  • Verify documentation (including upgrade notes)

Summary by CodeRabbit

  • New Features

    • Added a --warmup-records option to exclude an initial set of records from steady-state statistics; steady-state and final summaries are reported separately and annotated when applicable; one-time notice printed when warmup is used.
  • Bug Fixes

    • Validation tightened for warmup records (must be >= 0 and less than total records); improved statistics reporting behavior.
  • Tests

    • Added tests for warmup argument parsing, invalid inputs, and correct counting/reporting of warmup vs steady-state records.

matt-welch and others added 21 commits September 16, 2024 16:17
This commit should enable a test PR against the trunk
Now using an overallStats object with new constructor to contin the
combined stats at end of test and a single printTotal method.
Refactor to remove declaration of overallStats and limit its scope to
only the location where printTotal is called.
Set Stats combine-constructor as package-private.
Remove commented code.
- Fix callbacks after switch to steady state which were not being
  properly created.
- Add constant for DEFAULT_REPORTING_INTERVAL_MS
- Remove redundant producer.send calls
- Add ternerary operator in state assignment
- Add check for non-positive numRecords
- bytes are now properly accoutned for.
- Refactor to use boolean isSteadyState over warmupRecords to track
  if a Stats object is of steady state
Stats objects now represent the whole test with
'stats' and the steady state portion of the test
with 'steadyStateStats'. Remove merging of
Stats latency arrays at end of test.
- Add boolean to stats objects to control if they should print or not
- Add DEBUG print to show value of config.warmupRecords
- Use "long" for numRecords and warmupRecords instead of "Long"
  - Remove "* != null" for above
- Remove unused "combiner" constructor
- Remove DEFAULT_REPORTING_INTERVAL_MS as a constructor parameter and
  move into Stats class; refactor Stats constructors to not use
  reportingInterval as a parameter
- Remove DEBUG print statement
@arvi18
Copy link
Author

arvi18 commented Apr 26, 2025

I was able to reproduce and fix the performance issue. I had failed to create the record callback for messages in steady state :( It should be fixed in the latest push, but I still think the logic in that conditional can be improved.

re: checking warmup records before num records, I've added an exception for when numRecords <= 0.

FYI, I will be out for the next week, but thanks so much for looking at this patch!!

@arvi18
Copy link
Author

arvi18 commented Apr 26, 2025

Thanks for the latest changes. Results look much better now:

2000000 records sent, 9998.350272 records/sec (0.00 MB/sec), 0.58 ms avg latency, 417.00 ms max latency, 417 ms 50th, 417 ms 95th, 417 ms 99th, 417 ms 99.9th.
1500000 steady state records sent, 9997.733847 records/sec (0.95 MB/sec), 0.52 ms avg latency, 9.00 ms max latency, 1 ms 50th, 1 ms 95th, 1 ms 99th, 2 ms 99.9th.

There is still an issue with the overall throughput (0.00 MB/sec) though. I also see some checkstyle errors after rebasing.

FYI, I will be out for the next week, but thanks so much for looking at this patch!

No rush, take your time.

@arvi18
Copy link
Author

arvi18 commented Apr 26, 2025

Hi @matt-welch, there is one checkstyle error and you also probably need to rebase.

@arvi18
Copy link
Author

arvi18 commented Apr 26, 2025

@matt-welch sorry for the delay. Latest changes LGTM. Please, address the comments from @chia7712 and then I think we are good. Thanks.

PS: Feel free to ping people for review when you do changes, as we don't get notifications for new commits.

@arvi18
Copy link
Author

arvi18 commented Apr 26, 2025

Hi @chia7712 @fvaleri . My latest commit refactors the use of a warmupStats object and a main stats object in favor of having one stats that covers the whole test and a second object, steadyStateStats, that will be used during steady state operation.
In the case of a steady state record, I've added a new callback that will write into both data objects during steady state.
This new approach seems like a simpler, cleaner design and is much easier to understand what is happening. I've done some testing on both designs and they appear to perform similarly. My only reservation is that the producer will now have a larger memory requirement due to the redundant recording of latency data into the two objects. Let me know what you think.

@arvi18
Copy link
Author

arvi18 commented Apr 26, 2025

@matt-welch thanks for your update. I will take a look later!

@arvi18
Copy link
Author

arvi18 commented Apr 26, 2025

Hi @chia7712 and @fvaleri. I've made some updates to this PR in my latest commits including some sample output for review. Please let me know if there's anything else I can or should do to complete the PR or answer any concerns about it.
Thanks!

Copy link

coderabbitai bot commented Apr 26, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a --warmup-records option to ProducerPerformance, implements steady-state metrics separate from warmup, propagates latencies to both main and steady-state Stats, updates reporting and validation, and adds unit tests for parsing, validation, and steady-state behavior.

Changes

Cohort / File(s) Change Summary
ProducerPerformance core
tools/src/main/java/org/apache/kafka/tools/ProducerPerformance.java
Added --warmup-records CLI option, created steadyStateStats instance when warmup completes, wired steady-state propagation, and adjusted final/steady-state reporting.
Stats and callback
tools/src/main/java/org/apache/kafka/tools/Stats.java, tools/src/main/java/org/apache/kafka/tools/PerfCallback.java
Added steady-state-aware Stats constructors and flags (isSteadyState, steadyStateActive), updated record/printTotal behavior and reporting, and extended PerfCallback to update both Stats instances.
Config & arg parsing
tools/src/main/java/org/apache/kafka/tools/ConfigPostProcessor.java, tools/src/main/java/org/apache/kafka/tools/ProducerPerformance.java (argParser)
Added warmupRecords field, validation (>=0 and < numRecords), and --warmup-records argument registration with default 0.
Tests
tools/src/test/java/org/apache/kafka/tools/ProducerPerformanceTest.java
Added four tests validating parsing errors for invalid values, behavior for positive warmup, and negative warmup handling; mocks verify send counts and producer close.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User
    participant ProducerPerformance
    participant Stats_Main as Stats (main)
    participant Stats_Steady as Stats (steady)
    participant PerfCallback

    User->>ProducerPerformance: start with --warmup-records=N
    ProducerPerformance->>Stats_Main: initialize main Stats
    loop send records
        ProducerPerformance->>PerfCallback: send(record)
        PerfCallback->>Stats_Main: record(latency)
        alt warmup complete (after N)
            ProducerPerformance->>Stats_Steady: initialize steadyStateStats
        end
        alt steadyStateActive
            PerfCallback->>Stats_Steady: record(latency)
        end
    end
    ProducerPerformance->>Stats_Main: printTotal()
    alt steadyStateStats exists
        ProducerPerformance->>Stats_Steady: printTotal() (annotated "steady state")
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🐇
Warmup hops, then metrics race,
Steady-state finds its quiet place.
Latencies split, reports anew,
Tests check counts and closing too.
A little rabbit clap for the queue!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly and concisely conveys the primary change — enabling a warmup phase in the producer performance test — and includes the related JIRA (KAFKA-17645) and KIP (KIP-1052) identifiers. It accurately reflects the changes in ProducerPerformance and associated tests and is concise and single-line. This makes it suitable as the PR and commit summary for teammates scanning history.
Description Check ✅ Passed The PR description replaces the repository template with a focused explanation of the warmup feature, its runtime behavior, and how results are reported, and it documents that new unit tests were added. It also states a concise testing strategy and includes a committer checklist, meeting the repository's expectation to describe behavior changes and tests. Overall the description is sufficiently detailed to understand intent, implementation, and test coverage.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch clone-KIP-1052-PR

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@arvi18
Copy link
Author

arvi18 commented Apr 26, 2025

@matt-welch sorry for late review. could you please sync the trunk?

Copy link

refacto-test bot commented Jul 22, 2025

Refacto is reviewing this PR. Please wait for the review comments to be posted.

@visz11
Copy link
Collaborator

visz11 commented Sep 24, 2025

/refacto-visz

Copy link

refacto-visz bot commented Sep 24, 2025

Refacto is reviewing this PR. Please wait for the review comments to be posted.

Copy link

refacto-visz bot commented Sep 24, 2025

Code Review: Producer Performance Warmup Implementation

👍 Well Done
Steady State Metrics

Added capability to measure steady-state performance separately from warmup, improving test reliability and accuracy.

Comprehensive Error Handling

Added validation for warmup records parameter prevents invalid test configurations.

📌 Files Processed
  • tools/src/main/java/org/apache/kafka/tools/ProducerPerformance.java
  • tools/src/test/java/org/apache/kafka/tools/ProducerPerformanceTest.java
📝 Additional Comments
tools/src/main/java/org/apache/kafka/tools/ProducerPerformance.java (6)
Redundant Stats Calculation

Both stats and steadyStateStats objects record the same metrics for steady-state records, causing redundant calculations. This creates unnecessary CPU overhead as each record completion triggers duplicate processing in both stats objects after the warmup phase.

Standards:

  • ISO-IEC-25010-Performance-Efficiency-Resource-Utilization
  • Optimization-Pattern-Computational-Efficiency
  • Algorithmic-Complexity-Redundant-Operations
Redundant Condition Check

The condition 'this.isSteadyState || !this.steadyStateActive' is logically equivalent to '!(this.steadyStateActive && !this.isSteadyState)'. This complex condition could be simplified to improve readability while maintaining the same logical behavior.

Standards:

  • Algorithm-Correctness-Boolean-Logic
  • Logic-Verification-Simplification
Inconsistent Formatting

Inconsistent spacing in conditional statement (extra spaces around 'config.warmupRecords'). This minor formatting inconsistency makes the code slightly less readable and violates consistent style guidelines, potentially causing confusion during maintenance.

Standards:

  • Clean-Code-Formatting
  • Maintainability-Quality-Consistency
Potential Integer Overflow

The subtraction operation (numRecords - warmupRecords) could potentially cause integer overflow if warmupRecords is negative. While line 343 sets a default of 0 and line 558 checks if warmupRecords >= numRecords, there's no explicit check preventing negative warmupRecords values.

Standards:

  • CWE-190
  • OWASP-A03
Duplicate Code Blocks

The same code block for checking steadyStateStats and calling printTotal() is duplicated in two different branches. This duplication increases maintenance burden and risk of inconsistent updates, potentially leading to reliability issues if one block is updated but not the other.

Standards:

  • ISO-IEC-25010-Reliability-Maturity
  • ISO-IEC-25010-Functional-Correctness-Appropriateness
Duplicate Printout Calls

The code duplicates the same printTotal() logic in two places (lines 126-130 and 138-142), creating redundant code paths. This pattern increases maintenance burden and risks inconsistent behavior if one section is updated but not the other.

Standards:

  • ISO-IEC-25010-Performance-Efficiency-Maintainability
  • Optimization-Pattern-Code-Reuse
  • Algorithmic-Complexity-Duplicate-Logic

Comment on lines +101 to +105
if ( config.warmupRecords > 0 && i == config.warmupRecords ) {
steadyStateStats = new Stats(config.numRecords - config.warmupRecords, config.warmupRecords > 0);
stats.steadyStateActive = true;
}
cb = new PerfCallback(sendStartMs, payload.length, stats, steadyStateStats);
Copy link

Choose a reason for hiding this comment

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

Potential NullPointerException Risk

The steadyStateStats variable is passed to PerfCallback constructor even when it's null (when warmupRecords is 0 or i hasn't reached warmupRecords yet). This will cause NullPointerException in PerfCallback.onCompletion() when it tries to access steadyStateStats methods.

                if (config.warmupRecords > 0 && i == config.warmupRecords) {
                    steadyStateStats = new Stats(config.numRecords - config.warmupRecords, true);
                    stats.steadyStateActive = true;
                }
                Stats callbackSteadyStateStats = config.warmupRecords > 0 ? steadyStateStats : null;
                cb = new PerfCallback(sendStartMs, payload.length, stats, callbackSteadyStateStats);
Commitable Suggestion
Suggested change
if ( config.warmupRecords > 0 && i == config.warmupRecords ) {
steadyStateStats = new Stats(config.numRecords - config.warmupRecords, config.warmupRecords > 0);
stats.steadyStateActive = true;
}
cb = new PerfCallback(sendStartMs, payload.length, stats, steadyStateStats);
if (config.warmupRecords > 0 && i == config.warmupRecords) {
steadyStateStats = new Stats(config.numRecords - config.warmupRecords, true);
stats.steadyStateActive = true;
}
Stats callbackSteadyStateStats = config.warmupRecords > 0 ? steadyStateStats : null;
cb = new PerfCallback(sendStartMs, payload.length, stats, callbackSteadyStateStats);
Standards
  • ISO-IEC-25010-Reliability-Fault-Tolerance
  • ISO-IEC-25010-Functional-Correctness-Appropriateness
  • SRE-Error-Handling

Comment on lines +557 to +559
if (warmupRecords >= numRecords) {
throw new ArgumentParserException("The value for --warmup-records must be strictly fewer than the number of records in the test, --num-records.", parser);
}
Copy link

Choose a reason for hiding this comment

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

Warmup Validation Flaw

The validation allows negative warmup records to pass through since they're less than numRecords. While line 543 sets a default of 0 and the code later uses Math.max(warmupRecords, 0), this validation should explicitly check for negative values to prevent confusing behavior.

            if (warmupRecords < 0) {
                throw new ArgumentParserException("--warmup-records should be greater than or equal to zero", parser);
            }
            if (warmupRecords >= numRecords) {
                throw new ArgumentParserException("The value for --warmup-records must be strictly fewer than the number of records in the test, --num-records.", parser);
            }
Commitable Suggestion
Suggested change
if (warmupRecords >= numRecords) {
throw new ArgumentParserException("The value for --warmup-records must be strictly fewer than the number of records in the test, --num-records.", parser);
}
if (warmupRecords < 0) {
throw new ArgumentParserException("--warmup-records should be greater than or equal to zero", parser);
}
if (warmupRecords >= numRecords) {
throw new ArgumentParserException("The value for --warmup-records must be strictly fewer than the number of records in the test, --num-records.", parser);
}
Standards
  • Business-Rule-Input-Validation
  • Logic-Verification-Boundary-Conditions

Comment on lines +516 to +519
if (steadyStateStats != null) {
this.steadyStateStats.record(latency, bytes, now);
this.steadyStateStats.iteration++;
}
Copy link

Choose a reason for hiding this comment

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

Missing Initialization Check

The code checks if steadyStateStats is null before accessing it, but then uses this.steadyStateStats which might still be null if the field wasn't initialized. This inconsistent null check pattern could lead to NullPointerException.

                if (steadyStateStats != null) {
                    steadyStateStats.record(latency, bytes, now);
                    steadyStateStats.iteration++;
                }
Commitable Suggestion
Suggested change
if (steadyStateStats != null) {
this.steadyStateStats.record(latency, bytes, now);
this.steadyStateStats.iteration++;
}
if (steadyStateStats != null) {
steadyStateStats.record(latency, bytes, now);
steadyStateStats.iteration++;
}
Standards
  • Algorithm-Correctness-Null-Safety
  • Logic-Verification-Defensive-Programming


public Stats(long numRecords, int reportingInterval) {
public Stats(long numRecords, boolean isSteadyState) {
Copy link

Choose a reason for hiding this comment

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

Parameter Name Change

The constructor parameter was changed from 'reportingInterval' to 'isSteadyState' with different types (int to boolean), but the parameter is still used as reportingInterval in the constructor body. This creates a significant maintainability issue as the parameter name no longer reflects its usage.

        public Stats(long numRecords, boolean isSteadyState) {
            this.start = System.currentTimeMillis();
            this.windowStart = System.currentTimeMillis();
            this.iteration = 0;
            this.count = 0;
            this.bytes = 0;
            this.maxLatency = 0;
            this.totalLatency = 0;
            this.windowCount = 0;
            this.windowMaxLatency = 0;
            this.windowTotalLatency = 0;
            this.windowBytes = 0;
            this.totalLatency = 0;
            this.reportingInterval = 5000;
            this.isSteadyState = isSteadyState;
            this.steadyStateActive = isSteadyState;
        }
Commitable Suggestion
Suggested change
public Stats(long numRecords, boolean isSteadyState) {
public Stats(long numRecords, boolean isSteadyState) {
this.start = System.currentTimeMillis();
this.windowStart = System.currentTimeMillis();
this.iteration = 0;
this.count = 0;
this.bytes = 0;
this.maxLatency = 0;
this.totalLatency = 0;
this.windowCount = 0;
this.windowMaxLatency = 0;
this.windowTotalLatency = 0;
this.windowBytes = 0;
this.totalLatency = 0;
this.reportingInterval = 5000;
this.isSteadyState = isSteadyState;
this.steadyStateActive = isSteadyState;
}
Standards
  • Clean-Code-Meaningful-Names
  • SOLID-SRP
  • Maintainability-Quality-Interfaces

Comment on lines 125 to 133
/* print final results */
stats.printTotal();
/* print steady-state stats if relevant */
if (steadyStateStats != null) {
steadyStateStats.printTotal();
}
} else {
// Make sure all messages are sent before printing out the stats and the metrics
// We need to do this in a different branch for now since tests/kafkatest/sanity_checks/test_performance_services.py
Copy link

Choose a reason for hiding this comment

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

Duplicate Code

Identical code blocks for printing stats appear in both branches of the conditional. This violates the DRY principle and creates maintenance burden when changes to the stats printing logic are needed, requiring updates in multiple places.

Standards
  • Clean-Code-DRY
  • Refactoring-Extract-Method
  • Maintainability-Quality-Duplication

@@ -360,7 +392,9 @@ public Stats(long numRecords, int reportingInterval) {
this.windowTotalLatency = 0;
this.windowBytes = 0;
this.totalLatency = 0;
this.reportingInterval = reportingInterval;
this.reportingInterval = 5000;
Copy link

Choose a reason for hiding this comment

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

Hardcoded Value

The reporting interval is now hardcoded to 5000 instead of being passed as a parameter. This reduces flexibility and makes the code less configurable. The previous parameterized approach was more maintainable as it allowed for different reporting intervals without code changes.

Standards
  • Clean-Code-Configuration
  • Maintainability-Quality-Flexibility

Comment on lines 165 to +166
Stats stats;
Stats steadyStateStats;
Copy link

Choose a reason for hiding this comment

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

Inconsistent Variable Declaration

The steadyStateStats variable is declared but not initialized, while stats is similarly declared. This inconsistency in variable initialization pattern could lead to confusion and potential null pointer exceptions if not properly managed throughout the code.

Standards
  • ISO-IEC-25010-Reliability-Maturity
  • ISO-IEC-25010-Functional-Correctness-Appropriateness

Comment on lines +378 to +380
public Stats(long numRecords) {
this(numRecords, false);
}
Copy link

Choose a reason for hiding this comment

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

Redundant Parameter Check

The Stats constructor with one parameter calls the two-parameter constructor with false for isSteadyState, but later in the code, steadyStateActive is set based on config.warmupRecords > 0. This inconsistency in initialization could lead to incorrect behavior in steady state reporting.

Standards
  • ISO-IEC-25010-Functional-Correctness-Appropriateness
  • ISO-IEC-25010-Reliability-Maturity

if (config.warmupRecords > 0) {
System.out.println("Warmup first " + config.warmupRecords + " records. Steady state results will print after the complete test summary.");
}
stats = new Stats(config.numRecords);
Copy link

Choose a reason for hiding this comment

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

Stats Constructor Mismatch

The main Stats object is initialized with just numRecords, using the new constructor that defaults isSteadyState to false. However, the steadyStateActive field is later set to true on this object, creating an inconsistent state where the object is not initialized as steady state but is marked as active.

Standards
  • Logic-Verification-State-Consistency
  • Business-Rule-Object-Initialization

@visz11
Copy link
Collaborator

visz11 commented Sep 24, 2025

/refacto-visz

Copy link

refacto-visz bot commented Sep 24, 2025

PR already reviewed at the latest commit: 7def5f4.
Please try again with new changes.

….java

Co-authored-by: refacto-visz[bot] <230139763+refacto-visz[bot]@users.noreply.github.com>
@github-actions github-actions bot added the tools label Sep 24, 2025
@visz11
Copy link
Collaborator

visz11 commented Sep 24, 2025

/refacto-visz

Copy link

refacto-visz bot commented Sep 24, 2025

Refacto is reviewing this PR. Please wait for the review comments to be posted.

Copy link

refacto-visz bot commented Sep 24, 2025

Code Review: Warmup Records Implementation

👍 Well Done
Steady State Performance Tracking

The implementation of separate tracking for warmup and steady state metrics provides valuable performance insights while maintaining overall test statistics.

Input Validation Added

Added validation to prevent warmup records exceeding total records, improving parameter safety.

📌 Files Processed
  • tools/src/main/java/org/apache/kafka/tools/ProducerPerformance.java
  • tools/src/test/java/org/apache/kafka/tools/ProducerPerformanceTest.java
📝 Additional Comments
tools/src/main/java/org/apache/kafka/tools/ProducerPerformance.java (2)
Redundant Steady State Null Check

The steadyStateStats null check is redundant when warmupRecords > 0 because steadyStateStats is always initialized when warmupRecords > 0 and i reaches warmupRecords. This adds unnecessary conditional logic in the performance-critical path.

Standards:

  • ISO-IEC-25010-Performance-Time-Behaviour
  • Algorithm-Opt-Hash-Map
Redundant steadyStateActive field in Stats class

The Stats class has two boolean fields (isSteadyState and steadyStateActive) that appear to serve similar purposes. This redundancy creates confusion about when to use which field and increases the cognitive load for maintainers trying to understand the class's state.

Standards:

  • Clean-Code-Simplicity
  • SOLID-SRP
  • Refactoring-Remove-Redundancy

Comment on lines +517 to +520
if (steadyStateStats != null) {
this.steadyStateStats.record(latency, bytes, now);
this.steadyStateStats.iteration++;
}
Copy link

Choose a reason for hiding this comment

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

NullPointerException in PerfCallback.onCompletion

The code checks if steadyStateStats is null before accessing it, but then uses this.steadyStateStats which could still be null. This inconsistent null check pattern could lead to NullPointerException when steadyStateStats is null.

Standards
  • ISO-IEC-25010-Reliability-Fault-Tolerance
  • SRE-Error-Handling
  • DbC-Resource-Mgmt

Comment on lines +517 to +520
if (steadyStateStats != null) {
this.steadyStateStats.record(latency, bytes, now);
this.steadyStateStats.iteration++;
}
Copy link

Choose a reason for hiding this comment

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

Steady State Recording Logic

The code records all messages in steadyStateStats regardless of whether they're warmup or steady state records. This defeats the purpose of separating warmup from steady state measurements and will skew the steady state statistics.

Standards
  • Algorithm-Correctness-Data-Collection
  • Business-Rule-Measurement-Accuracy

Comment on lines +101 to +105
if ( config.warmupRecords > 0 && i == config.warmupRecords ) {
steadyStateStats = new Stats(config.numRecords - config.warmupRecords, config.warmupRecords > 0);
stats.steadyStateActive = true;
}
cb = new PerfCallback(sendStartMs, payload.length, stats, steadyStateStats);
Copy link

Choose a reason for hiding this comment

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

Inconsistent null handling for steadyStateStats

The steadyStateStats variable is passed to PerfCallback constructor before it's initialized when warmupRecords is 0 or before reaching the warmup threshold. This creates a risk of NullPointerException when the callback tries to access steadyStateStats methods.

Standards
  • Clean-Code-Defensive-Programming
  • SOLID-SRP
  • Refactoring-Null-Safety

steadyStateStats = new Stats(config.numRecords - config.warmupRecords, config.warmupRecords > 0);
stats.steadyStateActive = true;
}
cb = new PerfCallback(sendStartMs, payload.length, stats, steadyStateStats);
Copy link

Choose a reason for hiding this comment

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

Callback NPE Risk

The steadyStateStats variable is passed to PerfCallback constructor even when null (when warmupRecords is 0 or i hasn't reached warmupRecords). This creates a null pointer exception risk in PerfCallback.onCompletion() when accessing steadyStateStats methods.

Standards
  • CWE-476
  • OWASP-A06

Comment on lines +102 to +103
steadyStateStats = new Stats(config.numRecords - config.warmupRecords, config.warmupRecords > 0);
stats.steadyStateActive = true;
Copy link

Choose a reason for hiding this comment

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

Redundant Stats Initialization

The code initializes steadyStateStats with a boolean parameter derived from config.warmupRecords > 0, which is always true at this point. This redundant check creates confusion about the initialization state and could lead to inconsistent behavior.

Standards
  • Clean-Code-Logical-Clarity
  • Algorithm-Correctness-Initialization

Comment on lines +127 to +131
/* print steady-state stats if relevant */
/* print steady-state stats if warmup was configured */
if (config.warmupRecords > 0 && steadyStateStats != null) {
steadyStateStats.printTotal();
}
Copy link

Choose a reason for hiding this comment

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

Redundant steadyStateStats null checks across multiple code paths

The code contains redundant null checks for steadyStateStats across different code paths with inconsistent conditions. This creates maintenance challenges as changes need to be synchronized across multiple locations and increases cognitive complexity.

Standards
  • Clean-Code-DRY
  • SOLID-SRP
  • Refactoring-Extract-Method


public Stats(long numRecords, int reportingInterval) {
public Stats(long numRecords, boolean isSteadyState) {
Copy link

Choose a reason for hiding this comment

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

Inconsistent parameter naming in Stats constructor

The constructor parameter was changed from 'reportingInterval' to 'isSteadyState' with different types, but the parameter name no longer reflects its usage. This creates confusion as the parameter name suggests one purpose while the implementation uses it differently.

Standards
  • Clean-Code-Meaningful-Names
  • SOLID-SRP
  • Maintainability-Quality-Interfaces

Comment on lines +558 to +560
if (warmupRecords >= numRecords) {
throw new ArgumentParserException("The value for --warmup-records must be strictly fewer than the number of records in the test, --num-records.", parser);
}
Copy link

Choose a reason for hiding this comment

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

Negative Warmup Records Validation Missing

The validation allows negative warmup records to pass through since they're less than numRecords. While line 544 sets a default of 0 and Math.max is used later, explicit validation would prevent confusing behavior.

Standards
  • ISO-IEC-25010-Reliability-Fault-Tolerance
  • SRE-Input-Validation
  • DbC-Precondition

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
tools/src/main/java/org/apache/kafka/tools/ProducerPerformance.java (3)

101-106: Announce steady state at the cutover and isolate windows

Print “Beginning steady state.” immediately at the warmup boundary and reset the main stats window to avoid a mixed warmup/steady-state window. This guarantees the marker appears even for short runs and avoids delayed logging tied to the 5s window.

Apply this diff at the cutover:

-                if ( config.warmupRecords > 0 && i == config.warmupRecords ) {
-                    steadyStateStats = new Stats(config.numRecords - config.warmupRecords, config.warmupRecords > 0);
-                    stats.steadyStateActive = true;
-                }
+                if (config.warmupRecords > 0 && i == config.warmupRecords) {
+                    // Announce the cutover and reset the main window to prevent mixed warmup/steady-state window output
+                    System.out.println("Beginning steady state.");
+                    stats.newWindow();
+                    steadyStateStats = new Stats(config.numRecords - config.warmupRecords, true);
+                    stats.steadyStateActive = true;
+                }

416-421: Remove heuristic “Beginning steady state.” print from record()

This message will be printed immediately at the cutover (see prior suggestion). Keeping it here can delay the message up to one reporting interval and risks duplication.

-                if (this.isSteadyState && count == windowCount) {
-                    System.out.println("Beginning steady state.");
-                }
                 if (this.isSteadyState || !this.steadyStateActive) {
                     printWindow();
                 }

346-355: Clarify help text about negative values

Runtime coerces negative warmup values to zero via Math.max(...). Consider making that explicit in the CLI help to avoid surprises.

-               .help("The number of records to treat as warmup; these initial records will not be included in steady-state statistics. " +
-                       "An additional summary line will be printed describing the steady-state statistics. (default: 0).");
+               .help("The number of records to treat as warmup; these initial records will not be included in steady-state statistics. " +
+                       "Negative values are treated as 0. An additional summary line will be printed describing the steady-state statistics. (default: 0).");
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7def5f4 and 2026e14.

📒 Files selected for processing (1)
  • tools/src/main/java/org/apache/kafka/tools/ProducerPerformance.java (16 hunks)
🔇 Additional comments (7)
tools/src/main/java/org/apache/kafka/tools/ProducerPerformance.java (7)

79-82: LGTM: clear warmup notice and overall Stats initialization

The user-facing message is helpful, and keeping stats as the overall (warmup + steady state) aggregator aligns with the intended reporting semantics.


127-131: Simplify guard and reduce duplication when printing steady-state totals

config.warmupRecords > 0 is redundant since steadyStateStats is only non-null when warmup is configured. Also consider extracting a tiny helper to print totals to avoid duplicating this block in both branches.

-                /* print steady-state stats if warmup was configured */
-                if (config.warmupRecords > 0 && steadyStateStats != null) {
+                /* print steady-state stats if available */
+                if (steadyStateStats != null) {
                     steadyStateStats.printTotal();
                 }

140-143: DRY: consider extracting a helper to print end-of-test summaries

Both branches print overall totals and optionally steady-state totals. Extracting a small method (e.g., printSummaries(stats, steadyStateStats)) will keep behavior consistent and easier to maintain.


383-399: Make reporting interval a named constant

Hardcoding 5000 reduces clarity. Promote it to a constant for readability and discoverability.

-            this.reportingInterval = 5000;
+            this.reportingInterval = DEFAULT_REPORTING_INTERVAL_MS;

Add this near the top of the Stats class:

private static final long DEFAULT_REPORTING_INTERVAL_MS = 5000L;

471-474: LGTM: steady-state label in total summary

Clear labeling of steady-state totals improves readability of the final report.


500-506: LGTM: safe dual-recording into steadyStateStats after warmup

Null-guarding steadyStateStats ensures only steady-state records are captured; iteration increments remain consistent for both stats objects.

Also applies to: 517-520


545-560: LGTM: validation and clamping for warmupRecords

  • numRecords <= 0 check prevents degenerate runs.
  • Clamping warmupRecords to non-negative and enforcing warmupRecords < numRecords maintains clear semantics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants