Class: JmeterPerf::Report::Summary

Inherits:
Object
  • Object
show all
Defined in:
lib/jmeter_perf/report/summary.rb

Constant Summary collapse

JTL_HEADER =

JTL file headers used for parsing CSV rows

%i[
  timeStamp
  elapsed
  label
  responseCode
  responseMessage
  threadName
  dataType
  success
  failureMessage
  bytes
  sentBytes
  grpThreads
  allThreads
  URL
  Latency
  IdleTime
  Connect
]
CSV_HEADER_MAPPINGS =

Returns a mapping of CSV headers to their corresponding attribute symbols.

Returns:

  • (Hash<String, Symbol>)

    a mapping of CSV headers to their corresponding attribute symbols.

{
  "Name" => :name,
  "Average Response Time" => :avg,
  "Error Percentage" => :error_percentage,
  "Max Response Time" => :max,
  "Min Response Time" => :min,
  "10th Percentile" => :p10,
  "Median (50th Percentile)" => :p50,
  "95th Percentile" => :p95,
  "Requests Per Minute" => :requests_per_minute,
  "Standard Deviation" => :standard_deviation,
  "Total Run Time" => :total_run_time,
  "Total Bytes" => :total_bytes,
  "Total Errors" => :total_errors,
  "Total Latency" => :total_latency,
  "Total Requests" => :total_requests,
  "Total Sent Bytes" => :total_sent_bytes
  # "Response Code 200" => :response_codes["200"],
  # "Response Code 500" => :response_codes["500"] etc.
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(file_path:, name: nil, jtl_read_timeout: 30) ⇒ Summary

Initializes a new Summary instance for analyzing performance data.

Parameters:

  • file_path (String)

    the file path of the performance file to summarize. Either a JTL or CSV file.

  • name (String, nil) (defaults to: nil)

    an optional name for the summary, derived from the file path if not provided (default: nil)

  • jtl_read_timeout (Integer) (defaults to: 30)

    the maximum number of seconds to wait for a line read (default: 3)



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/jmeter_perf/report/summary.rb', line 135

def initialize(file_path:, name: nil, jtl_read_timeout: 30)
  @name = name || file_path.to_s.tr("/", "_")
  @jtl_read_timeout = jtl_read_timeout
  @finished = false
  @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

  @max = 0
  @min = 1_000_000
  @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
  @total_bytes = 0
  @total_errors = 0
  @total_latency = 0
  @total_requests = 0
  @total_sent_bytes = 0
  @csv_error_lines = []

  @file_path = file_path

  @start_time = nil
  @end_time = nil
end

Instance Attribute Details

#avgFloat

Returns the average response time.

Returns:

  • (Float)

    the average response time



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jmeter_perf/report/summary.rb', line 53

class Summary
  # JTL file headers used for parsing CSV rows
  JTL_HEADER = %i[
    timeStamp
    elapsed
    label
    responseCode
    responseMessage
    threadName
    dataType
    success
    failureMessage
    bytes
    sentBytes
    grpThreads
    allThreads
    URL
    Latency
    IdleTime
    Connect
  ]

  # @return [Hash<String, Symbol>] a mapping of CSV headers to their corresponding attribute symbols.
  CSV_HEADER_MAPPINGS = {
    "Name" => :name,
    "Average Response Time" => :avg,
    "Error Percentage" => :error_percentage,
    "Max Response Time" => :max,
    "Min Response Time" => :min,
    "10th Percentile" => :p10,
    "Median (50th Percentile)" => :p50,
    "95th Percentile" => :p95,
    "Requests Per Minute" => :requests_per_minute,
    "Standard Deviation" => :standard_deviation,
    "Total Run Time" => :total_run_time,
    "Total Bytes" => :total_bytes,
    "Total Errors" => :total_errors,
    "Total Latency" => :total_latency,
    "Total Requests" => :total_requests,
    "Total Sent Bytes" => :total_sent_bytes
    # "Response Code 200" => :response_codes["200"],
    # "Response Code 500" => :response_codes["500"] etc.
  }

  attr_accessor(*CSV_HEADER_MAPPINGS.values)
  # Response codes have multiple keys, so we need to handle them separately
  attr_accessor :response_codes
  # CSV Error Lines are an array of integers that get delimited by ":" when written to the CSV
  attr_accessor :csv_error_lines
  alias_method :rpm, :requests_per_minute
  alias_method :std, :standard_deviation
  alias_method :median, :p50

  # Reads a generated CSV report and sets all appropriate attributes.
  #
  # @param csv_path [String] the file path of the CSV report to read
  # @return [Summary] a new Summary instance with the parsed data
  def self.read(csv_path)
    summary = new(file_path: csv_path)
    CSV.foreach(csv_path, headers: true) do |row|
      metric = row["Metric"]
      value = row["Value"]

      if metric == "Name"
        summary.name = value
      elsif metric.start_with?("Response Code")
        code = metric.split.last
        summary.response_codes[code] = value.to_i
      elsif metric == "CSV Errors"
        summary.csv_error_lines = value.split(":").map(&:to_i)
      elsif CSV_HEADER_MAPPINGS.key?(metric)
        summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
      end
    end
    summary
  end

  # Initializes a new Summary instance for analyzing performance data.
  #
  # @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
  # @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
  # @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
  def initialize(file_path:, name: nil, jtl_read_timeout: 30)
    @name = name || file_path.to_s.tr("/", "_")
    @jtl_read_timeout = jtl_read_timeout
    @finished = false
    @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

    @max = 0
    @min = 1_000_000
    @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
    @total_bytes = 0
    @total_errors = 0
    @total_latency = 0
    @total_requests = 0
    @total_sent_bytes = 0
    @csv_error_lines = []

    @file_path = file_path

    @start_time = nil
    @end_time = nil
  end

  # Marks the summary as finished and joins the processing thread.
  #
  # @return [void]
  def finish!
    @finished = true
    @processing_jtl_thread&.join
  end

  # Generates a CSV report with the given output file.
  #
  # The CSV report includes the following:
  # - A header row with "Metric" and "Value".
  # - Rows for each metric and its corresponding value from `CSV_HEADER_MAPPINGS`.
  # - Rows for each response code and its count from `@response_codes`.
  # - A row for CSV errors, concatenated with ":".
  #
  # @param output_file [String] The path to the output CSV file.
  # @return [void]
  def write_csv(output_file)
    CSV.open(output_file, "wb") do |csv|
      csv << ["Metric", "Value"]
      CSV_HEADER_MAPPINGS.each do |metric, value|
        csv << [metric, public_send(value)]
      end
      @response_codes.each do |code, count|
        csv << ["Response Code #{code}", count]
      end

      csv << ["CSV Errors", @csv_error_lines.join(":")]
    end
  end

  # Starts streaming and processing JTL file content asynchronously.
  # @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
  # @return [void]
  def stream_jtl_async
    @processing_jtl_thread = Thread.new do
      Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
        sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
      end

      File.open(@file_path, "r") do |file|
        until @finished && file.eof?
          line = file.gets

          # Skip if the line is nil. This can happen if not @finished, and we are at EOF
          next if line.nil?
          # Process only if the line is complete. JMeter always finishes with a newline
          read_until_complete_line(file, line)
        end
      end
    end

    @processing_jtl_thread.abort_on_exception = true
    nil
  end

  # Summarizes the collected data by calculating statistical metrics and error rates.
  #
  # @return [void]
  def summarize_data!
    @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
    @error_percentage = (@total_errors.to_f / @total_requests) * 100
    @avg = @running_statistics_helper.avg
    @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
    @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
    @standard_deviation = @running_statistics_helper.std
  end

  private

  def read_until_complete_line(file, line)
    lineno = file.lineno
    return if lineno == 1 # Skip the header row
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
      # If finished and eof but no newline: Means processing was interrupted
      # JMeter always finishes with a new line in the JTL file
      until line.end_with?("\n") || (file.eof? && @finished)
        sleep 0.1
        line += file.gets.to_s
      end
    end
    parse_csv_row(line)
  rescue Timeout::Error
    raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
  rescue CSV::MalformedCSVError
    @csv_error_lines << file.lineno
  end

  def parse_csv_row(line)
    CSV.parse(line, headers: JTL_HEADER, liberal_parsing: true).each do |row|
      line_item = row.to_hash
      elapsed = line_item.fetch(:elapsed).to_i
      timestamp = line_item.fetch(:timeStamp).to_i

      # Update start and end times
      @start_time = timestamp if @start_time.nil? || timestamp < @start_time
      @end_time = timestamp + elapsed if @end_time.nil? || (timestamp + elapsed) > @end_time

      # Continue with processing the row as before...
      @running_statistics_helper.add_number(elapsed)
      @total_requests += 1
      @response_codes[line_item.fetch(:responseCode)] += 1
      @total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
      @total_bytes += line_item.fetch(:bytes, 0).to_i
      @total_sent_bytes += line_item.fetch(:sentBytes, 0).to_i
      @total_latency += line_item.fetch(:Latency).to_i
      @min = [@min, elapsed].min
      @max = [@max, elapsed].max
    end
  end
end

#csv_error_linesObject

CSV Error Lines are an array of integers that get delimited by “:” when written to the CSV



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jmeter_perf/report/summary.rb', line 53

class Summary
  # JTL file headers used for parsing CSV rows
  JTL_HEADER = %i[
    timeStamp
    elapsed
    label
    responseCode
    responseMessage
    threadName
    dataType
    success
    failureMessage
    bytes
    sentBytes
    grpThreads
    allThreads
    URL
    Latency
    IdleTime
    Connect
  ]

  # @return [Hash<String, Symbol>] a mapping of CSV headers to their corresponding attribute symbols.
  CSV_HEADER_MAPPINGS = {
    "Name" => :name,
    "Average Response Time" => :avg,
    "Error Percentage" => :error_percentage,
    "Max Response Time" => :max,
    "Min Response Time" => :min,
    "10th Percentile" => :p10,
    "Median (50th Percentile)" => :p50,
    "95th Percentile" => :p95,
    "Requests Per Minute" => :requests_per_minute,
    "Standard Deviation" => :standard_deviation,
    "Total Run Time" => :total_run_time,
    "Total Bytes" => :total_bytes,
    "Total Errors" => :total_errors,
    "Total Latency" => :total_latency,
    "Total Requests" => :total_requests,
    "Total Sent Bytes" => :total_sent_bytes
    # "Response Code 200" => :response_codes["200"],
    # "Response Code 500" => :response_codes["500"] etc.
  }

  attr_accessor(*CSV_HEADER_MAPPINGS.values)
  # Response codes have multiple keys, so we need to handle them separately
  attr_accessor :response_codes
  # CSV Error Lines are an array of integers that get delimited by ":" when written to the CSV
  attr_accessor :csv_error_lines
  alias_method :rpm, :requests_per_minute
  alias_method :std, :standard_deviation
  alias_method :median, :p50

  # Reads a generated CSV report and sets all appropriate attributes.
  #
  # @param csv_path [String] the file path of the CSV report to read
  # @return [Summary] a new Summary instance with the parsed data
  def self.read(csv_path)
    summary = new(file_path: csv_path)
    CSV.foreach(csv_path, headers: true) do |row|
      metric = row["Metric"]
      value = row["Value"]

      if metric == "Name"
        summary.name = value
      elsif metric.start_with?("Response Code")
        code = metric.split.last
        summary.response_codes[code] = value.to_i
      elsif metric == "CSV Errors"
        summary.csv_error_lines = value.split(":").map(&:to_i)
      elsif CSV_HEADER_MAPPINGS.key?(metric)
        summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
      end
    end
    summary
  end

  # Initializes a new Summary instance for analyzing performance data.
  #
  # @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
  # @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
  # @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
  def initialize(file_path:, name: nil, jtl_read_timeout: 30)
    @name = name || file_path.to_s.tr("/", "_")
    @jtl_read_timeout = jtl_read_timeout
    @finished = false
    @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

    @max = 0
    @min = 1_000_000
    @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
    @total_bytes = 0
    @total_errors = 0
    @total_latency = 0
    @total_requests = 0
    @total_sent_bytes = 0
    @csv_error_lines = []

    @file_path = file_path

    @start_time = nil
    @end_time = nil
  end

  # Marks the summary as finished and joins the processing thread.
  #
  # @return [void]
  def finish!
    @finished = true
    @processing_jtl_thread&.join
  end

  # Generates a CSV report with the given output file.
  #
  # The CSV report includes the following:
  # - A header row with "Metric" and "Value".
  # - Rows for each metric and its corresponding value from `CSV_HEADER_MAPPINGS`.
  # - Rows for each response code and its count from `@response_codes`.
  # - A row for CSV errors, concatenated with ":".
  #
  # @param output_file [String] The path to the output CSV file.
  # @return [void]
  def write_csv(output_file)
    CSV.open(output_file, "wb") do |csv|
      csv << ["Metric", "Value"]
      CSV_HEADER_MAPPINGS.each do |metric, value|
        csv << [metric, public_send(value)]
      end
      @response_codes.each do |code, count|
        csv << ["Response Code #{code}", count]
      end

      csv << ["CSV Errors", @csv_error_lines.join(":")]
    end
  end

  # Starts streaming and processing JTL file content asynchronously.
  # @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
  # @return [void]
  def stream_jtl_async
    @processing_jtl_thread = Thread.new do
      Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
        sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
      end

      File.open(@file_path, "r") do |file|
        until @finished && file.eof?
          line = file.gets

          # Skip if the line is nil. This can happen if not @finished, and we are at EOF
          next if line.nil?
          # Process only if the line is complete. JMeter always finishes with a newline
          read_until_complete_line(file, line)
        end
      end
    end

    @processing_jtl_thread.abort_on_exception = true
    nil
  end

  # Summarizes the collected data by calculating statistical metrics and error rates.
  #
  # @return [void]
  def summarize_data!
    @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
    @error_percentage = (@total_errors.to_f / @total_requests) * 100
    @avg = @running_statistics_helper.avg
    @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
    @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
    @standard_deviation = @running_statistics_helper.std
  end

  private

  def read_until_complete_line(file, line)
    lineno = file.lineno
    return if lineno == 1 # Skip the header row
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
      # If finished and eof but no newline: Means processing was interrupted
      # JMeter always finishes with a new line in the JTL file
      until line.end_with?("\n") || (file.eof? && @finished)
        sleep 0.1
        line += file.gets.to_s
      end
    end
    parse_csv_row(line)
  rescue Timeout::Error
    raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
  rescue CSV::MalformedCSVError
    @csv_error_lines << file.lineno
  end

  def parse_csv_row(line)
    CSV.parse(line, headers: JTL_HEADER, liberal_parsing: true).each do |row|
      line_item = row.to_hash
      elapsed = line_item.fetch(:elapsed).to_i
      timestamp = line_item.fetch(:timeStamp).to_i

      # Update start and end times
      @start_time = timestamp if @start_time.nil? || timestamp < @start_time
      @end_time = timestamp + elapsed if @end_time.nil? || (timestamp + elapsed) > @end_time

      # Continue with processing the row as before...
      @running_statistics_helper.add_number(elapsed)
      @total_requests += 1
      @response_codes[line_item.fetch(:responseCode)] += 1
      @total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
      @total_bytes += line_item.fetch(:bytes, 0).to_i
      @total_sent_bytes += line_item.fetch(:sentBytes, 0).to_i
      @total_latency += line_item.fetch(:Latency).to_i
      @min = [@min, elapsed].min
      @max = [@max, elapsed].max
    end
  end
end

#error_percentageFloat

Returns the error percentage across all requests.

Returns:

  • (Float)

    the error percentage across all requests



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jmeter_perf/report/summary.rb', line 53

class Summary
  # JTL file headers used for parsing CSV rows
  JTL_HEADER = %i[
    timeStamp
    elapsed
    label
    responseCode
    responseMessage
    threadName
    dataType
    success
    failureMessage
    bytes
    sentBytes
    grpThreads
    allThreads
    URL
    Latency
    IdleTime
    Connect
  ]

  # @return [Hash<String, Symbol>] a mapping of CSV headers to their corresponding attribute symbols.
  CSV_HEADER_MAPPINGS = {
    "Name" => :name,
    "Average Response Time" => :avg,
    "Error Percentage" => :error_percentage,
    "Max Response Time" => :max,
    "Min Response Time" => :min,
    "10th Percentile" => :p10,
    "Median (50th Percentile)" => :p50,
    "95th Percentile" => :p95,
    "Requests Per Minute" => :requests_per_minute,
    "Standard Deviation" => :standard_deviation,
    "Total Run Time" => :total_run_time,
    "Total Bytes" => :total_bytes,
    "Total Errors" => :total_errors,
    "Total Latency" => :total_latency,
    "Total Requests" => :total_requests,
    "Total Sent Bytes" => :total_sent_bytes
    # "Response Code 200" => :response_codes["200"],
    # "Response Code 500" => :response_codes["500"] etc.
  }

  attr_accessor(*CSV_HEADER_MAPPINGS.values)
  # Response codes have multiple keys, so we need to handle them separately
  attr_accessor :response_codes
  # CSV Error Lines are an array of integers that get delimited by ":" when written to the CSV
  attr_accessor :csv_error_lines
  alias_method :rpm, :requests_per_minute
  alias_method :std, :standard_deviation
  alias_method :median, :p50

  # Reads a generated CSV report and sets all appropriate attributes.
  #
  # @param csv_path [String] the file path of the CSV report to read
  # @return [Summary] a new Summary instance with the parsed data
  def self.read(csv_path)
    summary = new(file_path: csv_path)
    CSV.foreach(csv_path, headers: true) do |row|
      metric = row["Metric"]
      value = row["Value"]

      if metric == "Name"
        summary.name = value
      elsif metric.start_with?("Response Code")
        code = metric.split.last
        summary.response_codes[code] = value.to_i
      elsif metric == "CSV Errors"
        summary.csv_error_lines = value.split(":").map(&:to_i)
      elsif CSV_HEADER_MAPPINGS.key?(metric)
        summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
      end
    end
    summary
  end

  # Initializes a new Summary instance for analyzing performance data.
  #
  # @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
  # @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
  # @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
  def initialize(file_path:, name: nil, jtl_read_timeout: 30)
    @name = name || file_path.to_s.tr("/", "_")
    @jtl_read_timeout = jtl_read_timeout
    @finished = false
    @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

    @max = 0
    @min = 1_000_000
    @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
    @total_bytes = 0
    @total_errors = 0
    @total_latency = 0
    @total_requests = 0
    @total_sent_bytes = 0
    @csv_error_lines = []

    @file_path = file_path

    @start_time = nil
    @end_time = nil
  end

  # Marks the summary as finished and joins the processing thread.
  #
  # @return [void]
  def finish!
    @finished = true
    @processing_jtl_thread&.join
  end

  # Generates a CSV report with the given output file.
  #
  # The CSV report includes the following:
  # - A header row with "Metric" and "Value".
  # - Rows for each metric and its corresponding value from `CSV_HEADER_MAPPINGS`.
  # - Rows for each response code and its count from `@response_codes`.
  # - A row for CSV errors, concatenated with ":".
  #
  # @param output_file [String] The path to the output CSV file.
  # @return [void]
  def write_csv(output_file)
    CSV.open(output_file, "wb") do |csv|
      csv << ["Metric", "Value"]
      CSV_HEADER_MAPPINGS.each do |metric, value|
        csv << [metric, public_send(value)]
      end
      @response_codes.each do |code, count|
        csv << ["Response Code #{code}", count]
      end

      csv << ["CSV Errors", @csv_error_lines.join(":")]
    end
  end

  # Starts streaming and processing JTL file content asynchronously.
  # @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
  # @return [void]
  def stream_jtl_async
    @processing_jtl_thread = Thread.new do
      Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
        sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
      end

      File.open(@file_path, "r") do |file|
        until @finished && file.eof?
          line = file.gets

          # Skip if the line is nil. This can happen if not @finished, and we are at EOF
          next if line.nil?
          # Process only if the line is complete. JMeter always finishes with a newline
          read_until_complete_line(file, line)
        end
      end
    end

    @processing_jtl_thread.abort_on_exception = true
    nil
  end

  # Summarizes the collected data by calculating statistical metrics and error rates.
  #
  # @return [void]
  def summarize_data!
    @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
    @error_percentage = (@total_errors.to_f / @total_requests) * 100
    @avg = @running_statistics_helper.avg
    @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
    @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
    @standard_deviation = @running_statistics_helper.std
  end

  private

  def read_until_complete_line(file, line)
    lineno = file.lineno
    return if lineno == 1 # Skip the header row
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
      # If finished and eof but no newline: Means processing was interrupted
      # JMeter always finishes with a new line in the JTL file
      until line.end_with?("\n") || (file.eof? && @finished)
        sleep 0.1
        line += file.gets.to_s
      end
    end
    parse_csv_row(line)
  rescue Timeout::Error
    raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
  rescue CSV::MalformedCSVError
    @csv_error_lines << file.lineno
  end

  def parse_csv_row(line)
    CSV.parse(line, headers: JTL_HEADER, liberal_parsing: true).each do |row|
      line_item = row.to_hash
      elapsed = line_item.fetch(:elapsed).to_i
      timestamp = line_item.fetch(:timeStamp).to_i

      # Update start and end times
      @start_time = timestamp if @start_time.nil? || timestamp < @start_time
      @end_time = timestamp + elapsed if @end_time.nil? || (timestamp + elapsed) > @end_time

      # Continue with processing the row as before...
      @running_statistics_helper.add_number(elapsed)
      @total_requests += 1
      @response_codes[line_item.fetch(:responseCode)] += 1
      @total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
      @total_bytes += line_item.fetch(:bytes, 0).to_i
      @total_sent_bytes += line_item.fetch(:sentBytes, 0).to_i
      @total_latency += line_item.fetch(:Latency).to_i
      @min = [@min, elapsed].min
      @max = [@max, elapsed].max
    end
  end
end

#maxInteger

Returns the maximum response time encountered.

Returns:

  • (Integer)

    the maximum response time encountered



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jmeter_perf/report/summary.rb', line 53

class Summary
  # JTL file headers used for parsing CSV rows
  JTL_HEADER = %i[
    timeStamp
    elapsed
    label
    responseCode
    responseMessage
    threadName
    dataType
    success
    failureMessage
    bytes
    sentBytes
    grpThreads
    allThreads
    URL
    Latency
    IdleTime
    Connect
  ]

  # @return [Hash<String, Symbol>] a mapping of CSV headers to their corresponding attribute symbols.
  CSV_HEADER_MAPPINGS = {
    "Name" => :name,
    "Average Response Time" => :avg,
    "Error Percentage" => :error_percentage,
    "Max Response Time" => :max,
    "Min Response Time" => :min,
    "10th Percentile" => :p10,
    "Median (50th Percentile)" => :p50,
    "95th Percentile" => :p95,
    "Requests Per Minute" => :requests_per_minute,
    "Standard Deviation" => :standard_deviation,
    "Total Run Time" => :total_run_time,
    "Total Bytes" => :total_bytes,
    "Total Errors" => :total_errors,
    "Total Latency" => :total_latency,
    "Total Requests" => :total_requests,
    "Total Sent Bytes" => :total_sent_bytes
    # "Response Code 200" => :response_codes["200"],
    # "Response Code 500" => :response_codes["500"] etc.
  }

  attr_accessor(*CSV_HEADER_MAPPINGS.values)
  # Response codes have multiple keys, so we need to handle them separately
  attr_accessor :response_codes
  # CSV Error Lines are an array of integers that get delimited by ":" when written to the CSV
  attr_accessor :csv_error_lines
  alias_method :rpm, :requests_per_minute
  alias_method :std, :standard_deviation
  alias_method :median, :p50

  # Reads a generated CSV report and sets all appropriate attributes.
  #
  # @param csv_path [String] the file path of the CSV report to read
  # @return [Summary] a new Summary instance with the parsed data
  def self.read(csv_path)
    summary = new(file_path: csv_path)
    CSV.foreach(csv_path, headers: true) do |row|
      metric = row["Metric"]
      value = row["Value"]

      if metric == "Name"
        summary.name = value
      elsif metric.start_with?("Response Code")
        code = metric.split.last
        summary.response_codes[code] = value.to_i
      elsif metric == "CSV Errors"
        summary.csv_error_lines = value.split(":").map(&:to_i)
      elsif CSV_HEADER_MAPPINGS.key?(metric)
        summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
      end
    end
    summary
  end

  # Initializes a new Summary instance for analyzing performance data.
  #
  # @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
  # @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
  # @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
  def initialize(file_path:, name: nil, jtl_read_timeout: 30)
    @name = name || file_path.to_s.tr("/", "_")
    @jtl_read_timeout = jtl_read_timeout
    @finished = false
    @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

    @max = 0
    @min = 1_000_000
    @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
    @total_bytes = 0
    @total_errors = 0
    @total_latency = 0
    @total_requests = 0
    @total_sent_bytes = 0
    @csv_error_lines = []

    @file_path = file_path

    @start_time = nil
    @end_time = nil
  end

  # Marks the summary as finished and joins the processing thread.
  #
  # @return [void]
  def finish!
    @finished = true
    @processing_jtl_thread&.join
  end

  # Generates a CSV report with the given output file.
  #
  # The CSV report includes the following:
  # - A header row with "Metric" and "Value".
  # - Rows for each metric and its corresponding value from `CSV_HEADER_MAPPINGS`.
  # - Rows for each response code and its count from `@response_codes`.
  # - A row for CSV errors, concatenated with ":".
  #
  # @param output_file [String] The path to the output CSV file.
  # @return [void]
  def write_csv(output_file)
    CSV.open(output_file, "wb") do |csv|
      csv << ["Metric", "Value"]
      CSV_HEADER_MAPPINGS.each do |metric, value|
        csv << [metric, public_send(value)]
      end
      @response_codes.each do |code, count|
        csv << ["Response Code #{code}", count]
      end

      csv << ["CSV Errors", @csv_error_lines.join(":")]
    end
  end

  # Starts streaming and processing JTL file content asynchronously.
  # @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
  # @return [void]
  def stream_jtl_async
    @processing_jtl_thread = Thread.new do
      Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
        sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
      end

      File.open(@file_path, "r") do |file|
        until @finished && file.eof?
          line = file.gets

          # Skip if the line is nil. This can happen if not @finished, and we are at EOF
          next if line.nil?
          # Process only if the line is complete. JMeter always finishes with a newline
          read_until_complete_line(file, line)
        end
      end
    end

    @processing_jtl_thread.abort_on_exception = true
    nil
  end

  # Summarizes the collected data by calculating statistical metrics and error rates.
  #
  # @return [void]
  def summarize_data!
    @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
    @error_percentage = (@total_errors.to_f / @total_requests) * 100
    @avg = @running_statistics_helper.avg
    @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
    @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
    @standard_deviation = @running_statistics_helper.std
  end

  private

  def read_until_complete_line(file, line)
    lineno = file.lineno
    return if lineno == 1 # Skip the header row
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
      # If finished and eof but no newline: Means processing was interrupted
      # JMeter always finishes with a new line in the JTL file
      until line.end_with?("\n") || (file.eof? && @finished)
        sleep 0.1
        line += file.gets.to_s
      end
    end
    parse_csv_row(line)
  rescue Timeout::Error
    raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
  rescue CSV::MalformedCSVError
    @csv_error_lines << file.lineno
  end

  def parse_csv_row(line)
    CSV.parse(line, headers: JTL_HEADER, liberal_parsing: true).each do |row|
      line_item = row.to_hash
      elapsed = line_item.fetch(:elapsed).to_i
      timestamp = line_item.fetch(:timeStamp).to_i

      # Update start and end times
      @start_time = timestamp if @start_time.nil? || timestamp < @start_time
      @end_time = timestamp + elapsed if @end_time.nil? || (timestamp + elapsed) > @end_time

      # Continue with processing the row as before...
      @running_statistics_helper.add_number(elapsed)
      @total_requests += 1
      @response_codes[line_item.fetch(:responseCode)] += 1
      @total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
      @total_bytes += line_item.fetch(:bytes, 0).to_i
      @total_sent_bytes += line_item.fetch(:sentBytes, 0).to_i
      @total_latency += line_item.fetch(:Latency).to_i
      @min = [@min, elapsed].min
      @max = [@max, elapsed].max
    end
  end
end

#minInteger

Returns the minimum response time encountered.

Returns:

  • (Integer)

    the minimum response time encountered



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jmeter_perf/report/summary.rb', line 53

class Summary
  # JTL file headers used for parsing CSV rows
  JTL_HEADER = %i[
    timeStamp
    elapsed
    label
    responseCode
    responseMessage
    threadName
    dataType
    success
    failureMessage
    bytes
    sentBytes
    grpThreads
    allThreads
    URL
    Latency
    IdleTime
    Connect
  ]

  # @return [Hash<String, Symbol>] a mapping of CSV headers to their corresponding attribute symbols.
  CSV_HEADER_MAPPINGS = {
    "Name" => :name,
    "Average Response Time" => :avg,
    "Error Percentage" => :error_percentage,
    "Max Response Time" => :max,
    "Min Response Time" => :min,
    "10th Percentile" => :p10,
    "Median (50th Percentile)" => :p50,
    "95th Percentile" => :p95,
    "Requests Per Minute" => :requests_per_minute,
    "Standard Deviation" => :standard_deviation,
    "Total Run Time" => :total_run_time,
    "Total Bytes" => :total_bytes,
    "Total Errors" => :total_errors,
    "Total Latency" => :total_latency,
    "Total Requests" => :total_requests,
    "Total Sent Bytes" => :total_sent_bytes
    # "Response Code 200" => :response_codes["200"],
    # "Response Code 500" => :response_codes["500"] etc.
  }

  attr_accessor(*CSV_HEADER_MAPPINGS.values)
  # Response codes have multiple keys, so we need to handle them separately
  attr_accessor :response_codes
  # CSV Error Lines are an array of integers that get delimited by ":" when written to the CSV
  attr_accessor :csv_error_lines
  alias_method :rpm, :requests_per_minute
  alias_method :std, :standard_deviation
  alias_method :median, :p50

  # Reads a generated CSV report and sets all appropriate attributes.
  #
  # @param csv_path [String] the file path of the CSV report to read
  # @return [Summary] a new Summary instance with the parsed data
  def self.read(csv_path)
    summary = new(file_path: csv_path)
    CSV.foreach(csv_path, headers: true) do |row|
      metric = row["Metric"]
      value = row["Value"]

      if metric == "Name"
        summary.name = value
      elsif metric.start_with?("Response Code")
        code = metric.split.last
        summary.response_codes[code] = value.to_i
      elsif metric == "CSV Errors"
        summary.csv_error_lines = value.split(":").map(&:to_i)
      elsif CSV_HEADER_MAPPINGS.key?(metric)
        summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
      end
    end
    summary
  end

  # Initializes a new Summary instance for analyzing performance data.
  #
  # @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
  # @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
  # @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
  def initialize(file_path:, name: nil, jtl_read_timeout: 30)
    @name = name || file_path.to_s.tr("/", "_")
    @jtl_read_timeout = jtl_read_timeout
    @finished = false
    @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

    @max = 0
    @min = 1_000_000
    @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
    @total_bytes = 0
    @total_errors = 0
    @total_latency = 0
    @total_requests = 0
    @total_sent_bytes = 0
    @csv_error_lines = []

    @file_path = file_path

    @start_time = nil
    @end_time = nil
  end

  # Marks the summary as finished and joins the processing thread.
  #
  # @return [void]
  def finish!
    @finished = true
    @processing_jtl_thread&.join
  end

  # Generates a CSV report with the given output file.
  #
  # The CSV report includes the following:
  # - A header row with "Metric" and "Value".
  # - Rows for each metric and its corresponding value from `CSV_HEADER_MAPPINGS`.
  # - Rows for each response code and its count from `@response_codes`.
  # - A row for CSV errors, concatenated with ":".
  #
  # @param output_file [String] The path to the output CSV file.
  # @return [void]
  def write_csv(output_file)
    CSV.open(output_file, "wb") do |csv|
      csv << ["Metric", "Value"]
      CSV_HEADER_MAPPINGS.each do |metric, value|
        csv << [metric, public_send(value)]
      end
      @response_codes.each do |code, count|
        csv << ["Response Code #{code}", count]
      end

      csv << ["CSV Errors", @csv_error_lines.join(":")]
    end
  end

  # Starts streaming and processing JTL file content asynchronously.
  # @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
  # @return [void]
  def stream_jtl_async
    @processing_jtl_thread = Thread.new do
      Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
        sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
      end

      File.open(@file_path, "r") do |file|
        until @finished && file.eof?
          line = file.gets

          # Skip if the line is nil. This can happen if not @finished, and we are at EOF
          next if line.nil?
          # Process only if the line is complete. JMeter always finishes with a newline
          read_until_complete_line(file, line)
        end
      end
    end

    @processing_jtl_thread.abort_on_exception = true
    nil
  end

  # Summarizes the collected data by calculating statistical metrics and error rates.
  #
  # @return [void]
  def summarize_data!
    @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
    @error_percentage = (@total_errors.to_f / @total_requests) * 100
    @avg = @running_statistics_helper.avg
    @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
    @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
    @standard_deviation = @running_statistics_helper.std
  end

  private

  def read_until_complete_line(file, line)
    lineno = file.lineno
    return if lineno == 1 # Skip the header row
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
      # If finished and eof but no newline: Means processing was interrupted
      # JMeter always finishes with a new line in the JTL file
      until line.end_with?("\n") || (file.eof? && @finished)
        sleep 0.1
        line += file.gets.to_s
      end
    end
    parse_csv_row(line)
  rescue Timeout::Error
    raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
  rescue CSV::MalformedCSVError
    @csv_error_lines << file.lineno
  end

  def parse_csv_row(line)
    CSV.parse(line, headers: JTL_HEADER, liberal_parsing: true).each do |row|
      line_item = row.to_hash
      elapsed = line_item.fetch(:elapsed).to_i
      timestamp = line_item.fetch(:timeStamp).to_i

      # Update start and end times
      @start_time = timestamp if @start_time.nil? || timestamp < @start_time
      @end_time = timestamp + elapsed if @end_time.nil? || (timestamp + elapsed) > @end_time

      # Continue with processing the row as before...
      @running_statistics_helper.add_number(elapsed)
      @total_requests += 1
      @response_codes[line_item.fetch(:responseCode)] += 1
      @total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
      @total_bytes += line_item.fetch(:bytes, 0).to_i
      @total_sent_bytes += line_item.fetch(:sentBytes, 0).to_i
      @total_latency += line_item.fetch(:Latency).to_i
      @min = [@min, elapsed].min
      @max = [@max, elapsed].max
    end
  end
end

#nameString

Returns the name of the summary, derived from the file path if not provided.

Returns:

  • (String)

    the name of the summary, derived from the file path if not provided



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jmeter_perf/report/summary.rb', line 53

class Summary
  # JTL file headers used for parsing CSV rows
  JTL_HEADER = %i[
    timeStamp
    elapsed
    label
    responseCode
    responseMessage
    threadName
    dataType
    success
    failureMessage
    bytes
    sentBytes
    grpThreads
    allThreads
    URL
    Latency
    IdleTime
    Connect
  ]

  # @return [Hash<String, Symbol>] a mapping of CSV headers to their corresponding attribute symbols.
  CSV_HEADER_MAPPINGS = {
    "Name" => :name,
    "Average Response Time" => :avg,
    "Error Percentage" => :error_percentage,
    "Max Response Time" => :max,
    "Min Response Time" => :min,
    "10th Percentile" => :p10,
    "Median (50th Percentile)" => :p50,
    "95th Percentile" => :p95,
    "Requests Per Minute" => :requests_per_minute,
    "Standard Deviation" => :standard_deviation,
    "Total Run Time" => :total_run_time,
    "Total Bytes" => :total_bytes,
    "Total Errors" => :total_errors,
    "Total Latency" => :total_latency,
    "Total Requests" => :total_requests,
    "Total Sent Bytes" => :total_sent_bytes
    # "Response Code 200" => :response_codes["200"],
    # "Response Code 500" => :response_codes["500"] etc.
  }

  attr_accessor(*CSV_HEADER_MAPPINGS.values)
  # Response codes have multiple keys, so we need to handle them separately
  attr_accessor :response_codes
  # CSV Error Lines are an array of integers that get delimited by ":" when written to the CSV
  attr_accessor :csv_error_lines
  alias_method :rpm, :requests_per_minute
  alias_method :std, :standard_deviation
  alias_method :median, :p50

  # Reads a generated CSV report and sets all appropriate attributes.
  #
  # @param csv_path [String] the file path of the CSV report to read
  # @return [Summary] a new Summary instance with the parsed data
  def self.read(csv_path)
    summary = new(file_path: csv_path)
    CSV.foreach(csv_path, headers: true) do |row|
      metric = row["Metric"]
      value = row["Value"]

      if metric == "Name"
        summary.name = value
      elsif metric.start_with?("Response Code")
        code = metric.split.last
        summary.response_codes[code] = value.to_i
      elsif metric == "CSV Errors"
        summary.csv_error_lines = value.split(":").map(&:to_i)
      elsif CSV_HEADER_MAPPINGS.key?(metric)
        summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
      end
    end
    summary
  end

  # Initializes a new Summary instance for analyzing performance data.
  #
  # @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
  # @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
  # @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
  def initialize(file_path:, name: nil, jtl_read_timeout: 30)
    @name = name || file_path.to_s.tr("/", "_")
    @jtl_read_timeout = jtl_read_timeout
    @finished = false
    @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

    @max = 0
    @min = 1_000_000
    @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
    @total_bytes = 0
    @total_errors = 0
    @total_latency = 0
    @total_requests = 0
    @total_sent_bytes = 0
    @csv_error_lines = []

    @file_path = file_path

    @start_time = nil
    @end_time = nil
  end

  # Marks the summary as finished and joins the processing thread.
  #
  # @return [void]
  def finish!
    @finished = true
    @processing_jtl_thread&.join
  end

  # Generates a CSV report with the given output file.
  #
  # The CSV report includes the following:
  # - A header row with "Metric" and "Value".
  # - Rows for each metric and its corresponding value from `CSV_HEADER_MAPPINGS`.
  # - Rows for each response code and its count from `@response_codes`.
  # - A row for CSV errors, concatenated with ":".
  #
  # @param output_file [String] The path to the output CSV file.
  # @return [void]
  def write_csv(output_file)
    CSV.open(output_file, "wb") do |csv|
      csv << ["Metric", "Value"]
      CSV_HEADER_MAPPINGS.each do |metric, value|
        csv << [metric, public_send(value)]
      end
      @response_codes.each do |code, count|
        csv << ["Response Code #{code}", count]
      end

      csv << ["CSV Errors", @csv_error_lines.join(":")]
    end
  end

  # Starts streaming and processing JTL file content asynchronously.
  # @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
  # @return [void]
  def stream_jtl_async
    @processing_jtl_thread = Thread.new do
      Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
        sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
      end

      File.open(@file_path, "r") do |file|
        until @finished && file.eof?
          line = file.gets

          # Skip if the line is nil. This can happen if not @finished, and we are at EOF
          next if line.nil?
          # Process only if the line is complete. JMeter always finishes with a newline
          read_until_complete_line(file, line)
        end
      end
    end

    @processing_jtl_thread.abort_on_exception = true
    nil
  end

  # Summarizes the collected data by calculating statistical metrics and error rates.
  #
  # @return [void]
  def summarize_data!
    @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
    @error_percentage = (@total_errors.to_f / @total_requests) * 100
    @avg = @running_statistics_helper.avg
    @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
    @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
    @standard_deviation = @running_statistics_helper.std
  end

  private

  def read_until_complete_line(file, line)
    lineno = file.lineno
    return if lineno == 1 # Skip the header row
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
      # If finished and eof but no newline: Means processing was interrupted
      # JMeter always finishes with a new line in the JTL file
      until line.end_with?("\n") || (file.eof? && @finished)
        sleep 0.1
        line += file.gets.to_s
      end
    end
    parse_csv_row(line)
  rescue Timeout::Error
    raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
  rescue CSV::MalformedCSVError
    @csv_error_lines << file.lineno
  end

  def parse_csv_row(line)
    CSV.parse(line, headers: JTL_HEADER, liberal_parsing: true).each do |row|
      line_item = row.to_hash
      elapsed = line_item.fetch(:elapsed).to_i
      timestamp = line_item.fetch(:timeStamp).to_i

      # Update start and end times
      @start_time = timestamp if @start_time.nil? || timestamp < @start_time
      @end_time = timestamp + elapsed if @end_time.nil? || (timestamp + elapsed) > @end_time

      # Continue with processing the row as before...
      @running_statistics_helper.add_number(elapsed)
      @total_requests += 1
      @response_codes[line_item.fetch(:responseCode)] += 1
      @total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
      @total_bytes += line_item.fetch(:bytes, 0).to_i
      @total_sent_bytes += line_item.fetch(:sentBytes, 0).to_i
      @total_latency += line_item.fetch(:Latency).to_i
      @min = [@min, elapsed].min
      @max = [@max, elapsed].max
    end
  end
end

#p10Float

Returns the 10th percentile of response times.

Returns:

  • (Float)

    the 10th percentile of response times



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jmeter_perf/report/summary.rb', line 53

class Summary
  # JTL file headers used for parsing CSV rows
  JTL_HEADER = %i[
    timeStamp
    elapsed
    label
    responseCode
    responseMessage
    threadName
    dataType
    success
    failureMessage
    bytes
    sentBytes
    grpThreads
    allThreads
    URL
    Latency
    IdleTime
    Connect
  ]

  # @return [Hash<String, Symbol>] a mapping of CSV headers to their corresponding attribute symbols.
  CSV_HEADER_MAPPINGS = {
    "Name" => :name,
    "Average Response Time" => :avg,
    "Error Percentage" => :error_percentage,
    "Max Response Time" => :max,
    "Min Response Time" => :min,
    "10th Percentile" => :p10,
    "Median (50th Percentile)" => :p50,
    "95th Percentile" => :p95,
    "Requests Per Minute" => :requests_per_minute,
    "Standard Deviation" => :standard_deviation,
    "Total Run Time" => :total_run_time,
    "Total Bytes" => :total_bytes,
    "Total Errors" => :total_errors,
    "Total Latency" => :total_latency,
    "Total Requests" => :total_requests,
    "Total Sent Bytes" => :total_sent_bytes
    # "Response Code 200" => :response_codes["200"],
    # "Response Code 500" => :response_codes["500"] etc.
  }

  attr_accessor(*CSV_HEADER_MAPPINGS.values)
  # Response codes have multiple keys, so we need to handle them separately
  attr_accessor :response_codes
  # CSV Error Lines are an array of integers that get delimited by ":" when written to the CSV
  attr_accessor :csv_error_lines
  alias_method :rpm, :requests_per_minute
  alias_method :std, :standard_deviation
  alias_method :median, :p50

  # Reads a generated CSV report and sets all appropriate attributes.
  #
  # @param csv_path [String] the file path of the CSV report to read
  # @return [Summary] a new Summary instance with the parsed data
  def self.read(csv_path)
    summary = new(file_path: csv_path)
    CSV.foreach(csv_path, headers: true) do |row|
      metric = row["Metric"]
      value = row["Value"]

      if metric == "Name"
        summary.name = value
      elsif metric.start_with?("Response Code")
        code = metric.split.last
        summary.response_codes[code] = value.to_i
      elsif metric == "CSV Errors"
        summary.csv_error_lines = value.split(":").map(&:to_i)
      elsif CSV_HEADER_MAPPINGS.key?(metric)
        summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
      end
    end
    summary
  end

  # Initializes a new Summary instance for analyzing performance data.
  #
  # @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
  # @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
  # @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
  def initialize(file_path:, name: nil, jtl_read_timeout: 30)
    @name = name || file_path.to_s.tr("/", "_")
    @jtl_read_timeout = jtl_read_timeout
    @finished = false
    @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

    @max = 0
    @min = 1_000_000
    @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
    @total_bytes = 0
    @total_errors = 0
    @total_latency = 0
    @total_requests = 0
    @total_sent_bytes = 0
    @csv_error_lines = []

    @file_path = file_path

    @start_time = nil
    @end_time = nil
  end

  # Marks the summary as finished and joins the processing thread.
  #
  # @return [void]
  def finish!
    @finished = true
    @processing_jtl_thread&.join
  end

  # Generates a CSV report with the given output file.
  #
  # The CSV report includes the following:
  # - A header row with "Metric" and "Value".
  # - Rows for each metric and its corresponding value from `CSV_HEADER_MAPPINGS`.
  # - Rows for each response code and its count from `@response_codes`.
  # - A row for CSV errors, concatenated with ":".
  #
  # @param output_file [String] The path to the output CSV file.
  # @return [void]
  def write_csv(output_file)
    CSV.open(output_file, "wb") do |csv|
      csv << ["Metric", "Value"]
      CSV_HEADER_MAPPINGS.each do |metric, value|
        csv << [metric, public_send(value)]
      end
      @response_codes.each do |code, count|
        csv << ["Response Code #{code}", count]
      end

      csv << ["CSV Errors", @csv_error_lines.join(":")]
    end
  end

  # Starts streaming and processing JTL file content asynchronously.
  # @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
  # @return [void]
  def stream_jtl_async
    @processing_jtl_thread = Thread.new do
      Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
        sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
      end

      File.open(@file_path, "r") do |file|
        until @finished && file.eof?
          line = file.gets

          # Skip if the line is nil. This can happen if not @finished, and we are at EOF
          next if line.nil?
          # Process only if the line is complete. JMeter always finishes with a newline
          read_until_complete_line(file, line)
        end
      end
    end

    @processing_jtl_thread.abort_on_exception = true
    nil
  end

  # Summarizes the collected data by calculating statistical metrics and error rates.
  #
  # @return [void]
  def summarize_data!
    @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
    @error_percentage = (@total_errors.to_f / @total_requests) * 100
    @avg = @running_statistics_helper.avg
    @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
    @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
    @standard_deviation = @running_statistics_helper.std
  end

  private

  def read_until_complete_line(file, line)
    lineno = file.lineno
    return if lineno == 1 # Skip the header row
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
      # If finished and eof but no newline: Means processing was interrupted
      # JMeter always finishes with a new line in the JTL file
      until line.end_with?("\n") || (file.eof? && @finished)
        sleep 0.1
        line += file.gets.to_s
      end
    end
    parse_csv_row(line)
  rescue Timeout::Error
    raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
  rescue CSV::MalformedCSVError
    @csv_error_lines << file.lineno
  end

  def parse_csv_row(line)
    CSV.parse(line, headers: JTL_HEADER, liberal_parsing: true).each do |row|
      line_item = row.to_hash
      elapsed = line_item.fetch(:elapsed).to_i
      timestamp = line_item.fetch(:timeStamp).to_i

      # Update start and end times
      @start_time = timestamp if @start_time.nil? || timestamp < @start_time
      @end_time = timestamp + elapsed if @end_time.nil? || (timestamp + elapsed) > @end_time

      # Continue with processing the row as before...
      @running_statistics_helper.add_number(elapsed)
      @total_requests += 1
      @response_codes[line_item.fetch(:responseCode)] += 1
      @total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
      @total_bytes += line_item.fetch(:bytes, 0).to_i
      @total_sent_bytes += line_item.fetch(:sentBytes, 0).to_i
      @total_latency += line_item.fetch(:Latency).to_i
      @min = [@min, elapsed].min
      @max = [@max, elapsed].max
    end
  end
end

#p50Float Also known as: median

Returns the median (50th percentile) of response times.

Returns:

  • (Float)

    the median (50th percentile) of response times



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jmeter_perf/report/summary.rb', line 53

class Summary
  # JTL file headers used for parsing CSV rows
  JTL_HEADER = %i[
    timeStamp
    elapsed
    label
    responseCode
    responseMessage
    threadName
    dataType
    success
    failureMessage
    bytes
    sentBytes
    grpThreads
    allThreads
    URL
    Latency
    IdleTime
    Connect
  ]

  # @return [Hash<String, Symbol>] a mapping of CSV headers to their corresponding attribute symbols.
  CSV_HEADER_MAPPINGS = {
    "Name" => :name,
    "Average Response Time" => :avg,
    "Error Percentage" => :error_percentage,
    "Max Response Time" => :max,
    "Min Response Time" => :min,
    "10th Percentile" => :p10,
    "Median (50th Percentile)" => :p50,
    "95th Percentile" => :p95,
    "Requests Per Minute" => :requests_per_minute,
    "Standard Deviation" => :standard_deviation,
    "Total Run Time" => :total_run_time,
    "Total Bytes" => :total_bytes,
    "Total Errors" => :total_errors,
    "Total Latency" => :total_latency,
    "Total Requests" => :total_requests,
    "Total Sent Bytes" => :total_sent_bytes
    # "Response Code 200" => :response_codes["200"],
    # "Response Code 500" => :response_codes["500"] etc.
  }

  attr_accessor(*CSV_HEADER_MAPPINGS.values)
  # Response codes have multiple keys, so we need to handle them separately
  attr_accessor :response_codes
  # CSV Error Lines are an array of integers that get delimited by ":" when written to the CSV
  attr_accessor :csv_error_lines
  alias_method :rpm, :requests_per_minute
  alias_method :std, :standard_deviation
  alias_method :median, :p50

  # Reads a generated CSV report and sets all appropriate attributes.
  #
  # @param csv_path [String] the file path of the CSV report to read
  # @return [Summary] a new Summary instance with the parsed data
  def self.read(csv_path)
    summary = new(file_path: csv_path)
    CSV.foreach(csv_path, headers: true) do |row|
      metric = row["Metric"]
      value = row["Value"]

      if metric == "Name"
        summary.name = value
      elsif metric.start_with?("Response Code")
        code = metric.split.last
        summary.response_codes[code] = value.to_i
      elsif metric == "CSV Errors"
        summary.csv_error_lines = value.split(":").map(&:to_i)
      elsif CSV_HEADER_MAPPINGS.key?(metric)
        summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
      end
    end
    summary
  end

  # Initializes a new Summary instance for analyzing performance data.
  #
  # @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
  # @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
  # @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
  def initialize(file_path:, name: nil, jtl_read_timeout: 30)
    @name = name || file_path.to_s.tr("/", "_")
    @jtl_read_timeout = jtl_read_timeout
    @finished = false
    @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

    @max = 0
    @min = 1_000_000
    @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
    @total_bytes = 0
    @total_errors = 0
    @total_latency = 0
    @total_requests = 0
    @total_sent_bytes = 0
    @csv_error_lines = []

    @file_path = file_path

    @start_time = nil
    @end_time = nil
  end

  # Marks the summary as finished and joins the processing thread.
  #
  # @return [void]
  def finish!
    @finished = true
    @processing_jtl_thread&.join
  end

  # Generates a CSV report with the given output file.
  #
  # The CSV report includes the following:
  # - A header row with "Metric" and "Value".
  # - Rows for each metric and its corresponding value from `CSV_HEADER_MAPPINGS`.
  # - Rows for each response code and its count from `@response_codes`.
  # - A row for CSV errors, concatenated with ":".
  #
  # @param output_file [String] The path to the output CSV file.
  # @return [void]
  def write_csv(output_file)
    CSV.open(output_file, "wb") do |csv|
      csv << ["Metric", "Value"]
      CSV_HEADER_MAPPINGS.each do |metric, value|
        csv << [metric, public_send(value)]
      end
      @response_codes.each do |code, count|
        csv << ["Response Code #{code}", count]
      end

      csv << ["CSV Errors", @csv_error_lines.join(":")]
    end
  end

  # Starts streaming and processing JTL file content asynchronously.
  # @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
  # @return [void]
  def stream_jtl_async
    @processing_jtl_thread = Thread.new do
      Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
        sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
      end

      File.open(@file_path, "r") do |file|
        until @finished && file.eof?
          line = file.gets

          # Skip if the line is nil. This can happen if not @finished, and we are at EOF
          next if line.nil?
          # Process only if the line is complete. JMeter always finishes with a newline
          read_until_complete_line(file, line)
        end
      end
    end

    @processing_jtl_thread.abort_on_exception = true
    nil
  end

  # Summarizes the collected data by calculating statistical metrics and error rates.
  #
  # @return [void]
  def summarize_data!
    @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
    @error_percentage = (@total_errors.to_f / @total_requests) * 100
    @avg = @running_statistics_helper.avg
    @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
    @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
    @standard_deviation = @running_statistics_helper.std
  end

  private

  def read_until_complete_line(file, line)
    lineno = file.lineno
    return if lineno == 1 # Skip the header row
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
      # If finished and eof but no newline: Means processing was interrupted
      # JMeter always finishes with a new line in the JTL file
      until line.end_with?("\n") || (file.eof? && @finished)
        sleep 0.1
        line += file.gets.to_s
      end
    end
    parse_csv_row(line)
  rescue Timeout::Error
    raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
  rescue CSV::MalformedCSVError
    @csv_error_lines << file.lineno
  end

  def parse_csv_row(line)
    CSV.parse(line, headers: JTL_HEADER, liberal_parsing: true).each do |row|
      line_item = row.to_hash
      elapsed = line_item.fetch(:elapsed).to_i
      timestamp = line_item.fetch(:timeStamp).to_i

      # Update start and end times
      @start_time = timestamp if @start_time.nil? || timestamp < @start_time
      @end_time = timestamp + elapsed if @end_time.nil? || (timestamp + elapsed) > @end_time

      # Continue with processing the row as before...
      @running_statistics_helper.add_number(elapsed)
      @total_requests += 1
      @response_codes[line_item.fetch(:responseCode)] += 1
      @total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
      @total_bytes += line_item.fetch(:bytes, 0).to_i
      @total_sent_bytes += line_item.fetch(:sentBytes, 0).to_i
      @total_latency += line_item.fetch(:Latency).to_i
      @min = [@min, elapsed].min
      @max = [@max, elapsed].max
    end
  end
end

#p95Float

Returns the 95th percentile of response times.

Returns:

  • (Float)

    the 95th percentile of response times



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jmeter_perf/report/summary.rb', line 53

class Summary
  # JTL file headers used for parsing CSV rows
  JTL_HEADER = %i[
    timeStamp
    elapsed
    label
    responseCode
    responseMessage
    threadName
    dataType
    success
    failureMessage
    bytes
    sentBytes
    grpThreads
    allThreads
    URL
    Latency
    IdleTime
    Connect
  ]

  # @return [Hash<String, Symbol>] a mapping of CSV headers to their corresponding attribute symbols.
  CSV_HEADER_MAPPINGS = {
    "Name" => :name,
    "Average Response Time" => :avg,
    "Error Percentage" => :error_percentage,
    "Max Response Time" => :max,
    "Min Response Time" => :min,
    "10th Percentile" => :p10,
    "Median (50th Percentile)" => :p50,
    "95th Percentile" => :p95,
    "Requests Per Minute" => :requests_per_minute,
    "Standard Deviation" => :standard_deviation,
    "Total Run Time" => :total_run_time,
    "Total Bytes" => :total_bytes,
    "Total Errors" => :total_errors,
    "Total Latency" => :total_latency,
    "Total Requests" => :total_requests,
    "Total Sent Bytes" => :total_sent_bytes
    # "Response Code 200" => :response_codes["200"],
    # "Response Code 500" => :response_codes["500"] etc.
  }

  attr_accessor(*CSV_HEADER_MAPPINGS.values)
  # Response codes have multiple keys, so we need to handle them separately
  attr_accessor :response_codes
  # CSV Error Lines are an array of integers that get delimited by ":" when written to the CSV
  attr_accessor :csv_error_lines
  alias_method :rpm, :requests_per_minute
  alias_method :std, :standard_deviation
  alias_method :median, :p50

  # Reads a generated CSV report and sets all appropriate attributes.
  #
  # @param csv_path [String] the file path of the CSV report to read
  # @return [Summary] a new Summary instance with the parsed data
  def self.read(csv_path)
    summary = new(file_path: csv_path)
    CSV.foreach(csv_path, headers: true) do |row|
      metric = row["Metric"]
      value = row["Value"]

      if metric == "Name"
        summary.name = value
      elsif metric.start_with?("Response Code")
        code = metric.split.last
        summary.response_codes[code] = value.to_i
      elsif metric == "CSV Errors"
        summary.csv_error_lines = value.split(":").map(&:to_i)
      elsif CSV_HEADER_MAPPINGS.key?(metric)
        summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
      end
    end
    summary
  end

  # Initializes a new Summary instance for analyzing performance data.
  #
  # @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
  # @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
  # @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
  def initialize(file_path:, name: nil, jtl_read_timeout: 30)
    @name = name || file_path.to_s.tr("/", "_")
    @jtl_read_timeout = jtl_read_timeout
    @finished = false
    @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

    @max = 0
    @min = 1_000_000
    @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
    @total_bytes = 0
    @total_errors = 0
    @total_latency = 0
    @total_requests = 0
    @total_sent_bytes = 0
    @csv_error_lines = []

    @file_path = file_path

    @start_time = nil
    @end_time = nil
  end

  # Marks the summary as finished and joins the processing thread.
  #
  # @return [void]
  def finish!
    @finished = true
    @processing_jtl_thread&.join
  end

  # Generates a CSV report with the given output file.
  #
  # The CSV report includes the following:
  # - A header row with "Metric" and "Value".
  # - Rows for each metric and its corresponding value from `CSV_HEADER_MAPPINGS`.
  # - Rows for each response code and its count from `@response_codes`.
  # - A row for CSV errors, concatenated with ":".
  #
  # @param output_file [String] The path to the output CSV file.
  # @return [void]
  def write_csv(output_file)
    CSV.open(output_file, "wb") do |csv|
      csv << ["Metric", "Value"]
      CSV_HEADER_MAPPINGS.each do |metric, value|
        csv << [metric, public_send(value)]
      end
      @response_codes.each do |code, count|
        csv << ["Response Code #{code}", count]
      end

      csv << ["CSV Errors", @csv_error_lines.join(":")]
    end
  end

  # Starts streaming and processing JTL file content asynchronously.
  # @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
  # @return [void]
  def stream_jtl_async
    @processing_jtl_thread = Thread.new do
      Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
        sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
      end

      File.open(@file_path, "r") do |file|
        until @finished && file.eof?
          line = file.gets

          # Skip if the line is nil. This can happen if not @finished, and we are at EOF
          next if line.nil?
          # Process only if the line is complete. JMeter always finishes with a newline
          read_until_complete_line(file, line)
        end
      end
    end

    @processing_jtl_thread.abort_on_exception = true
    nil
  end

  # Summarizes the collected data by calculating statistical metrics and error rates.
  #
  # @return [void]
  def summarize_data!
    @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
    @error_percentage = (@total_errors.to_f / @total_requests) * 100
    @avg = @running_statistics_helper.avg
    @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
    @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
    @standard_deviation = @running_statistics_helper.std
  end

  private

  def read_until_complete_line(file, line)
    lineno = file.lineno
    return if lineno == 1 # Skip the header row
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
      # If finished and eof but no newline: Means processing was interrupted
      # JMeter always finishes with a new line in the JTL file
      until line.end_with?("\n") || (file.eof? && @finished)
        sleep 0.1
        line += file.gets.to_s
      end
    end
    parse_csv_row(line)
  rescue Timeout::Error
    raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
  rescue CSV::MalformedCSVError
    @csv_error_lines << file.lineno
  end

  def parse_csv_row(line)
    CSV.parse(line, headers: JTL_HEADER, liberal_parsing: true).each do |row|
      line_item = row.to_hash
      elapsed = line_item.fetch(:elapsed).to_i
      timestamp = line_item.fetch(:timeStamp).to_i

      # Update start and end times
      @start_time = timestamp if @start_time.nil? || timestamp < @start_time
      @end_time = timestamp + elapsed if @end_time.nil? || (timestamp + elapsed) > @end_time

      # Continue with processing the row as before...
      @running_statistics_helper.add_number(elapsed)
      @total_requests += 1
      @response_codes[line_item.fetch(:responseCode)] += 1
      @total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
      @total_bytes += line_item.fetch(:bytes, 0).to_i
      @total_sent_bytes += line_item.fetch(:sentBytes, 0).to_i
      @total_latency += line_item.fetch(:Latency).to_i
      @min = [@min, elapsed].min
      @max = [@max, elapsed].max
    end
  end
end

#requests_per_minuteFloat Also known as: rpm

Returns the requests per minute rate.

Returns:

  • (Float)

    the requests per minute rate



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jmeter_perf/report/summary.rb', line 53

class Summary
  # JTL file headers used for parsing CSV rows
  JTL_HEADER = %i[
    timeStamp
    elapsed
    label
    responseCode
    responseMessage
    threadName
    dataType
    success
    failureMessage
    bytes
    sentBytes
    grpThreads
    allThreads
    URL
    Latency
    IdleTime
    Connect
  ]

  # @return [Hash<String, Symbol>] a mapping of CSV headers to their corresponding attribute symbols.
  CSV_HEADER_MAPPINGS = {
    "Name" => :name,
    "Average Response Time" => :avg,
    "Error Percentage" => :error_percentage,
    "Max Response Time" => :max,
    "Min Response Time" => :min,
    "10th Percentile" => :p10,
    "Median (50th Percentile)" => :p50,
    "95th Percentile" => :p95,
    "Requests Per Minute" => :requests_per_minute,
    "Standard Deviation" => :standard_deviation,
    "Total Run Time" => :total_run_time,
    "Total Bytes" => :total_bytes,
    "Total Errors" => :total_errors,
    "Total Latency" => :total_latency,
    "Total Requests" => :total_requests,
    "Total Sent Bytes" => :total_sent_bytes
    # "Response Code 200" => :response_codes["200"],
    # "Response Code 500" => :response_codes["500"] etc.
  }

  attr_accessor(*CSV_HEADER_MAPPINGS.values)
  # Response codes have multiple keys, so we need to handle them separately
  attr_accessor :response_codes
  # CSV Error Lines are an array of integers that get delimited by ":" when written to the CSV
  attr_accessor :csv_error_lines
  alias_method :rpm, :requests_per_minute
  alias_method :std, :standard_deviation
  alias_method :median, :p50

  # Reads a generated CSV report and sets all appropriate attributes.
  #
  # @param csv_path [String] the file path of the CSV report to read
  # @return [Summary] a new Summary instance with the parsed data
  def self.read(csv_path)
    summary = new(file_path: csv_path)
    CSV.foreach(csv_path, headers: true) do |row|
      metric = row["Metric"]
      value = row["Value"]

      if metric == "Name"
        summary.name = value
      elsif metric.start_with?("Response Code")
        code = metric.split.last
        summary.response_codes[code] = value.to_i
      elsif metric == "CSV Errors"
        summary.csv_error_lines = value.split(":").map(&:to_i)
      elsif CSV_HEADER_MAPPINGS.key?(metric)
        summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
      end
    end
    summary
  end

  # Initializes a new Summary instance for analyzing performance data.
  #
  # @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
  # @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
  # @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
  def initialize(file_path:, name: nil, jtl_read_timeout: 30)
    @name = name || file_path.to_s.tr("/", "_")
    @jtl_read_timeout = jtl_read_timeout
    @finished = false
    @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

    @max = 0
    @min = 1_000_000
    @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
    @total_bytes = 0
    @total_errors = 0
    @total_latency = 0
    @total_requests = 0
    @total_sent_bytes = 0
    @csv_error_lines = []

    @file_path = file_path

    @start_time = nil
    @end_time = nil
  end

  # Marks the summary as finished and joins the processing thread.
  #
  # @return [void]
  def finish!
    @finished = true
    @processing_jtl_thread&.join
  end

  # Generates a CSV report with the given output file.
  #
  # The CSV report includes the following:
  # - A header row with "Metric" and "Value".
  # - Rows for each metric and its corresponding value from `CSV_HEADER_MAPPINGS`.
  # - Rows for each response code and its count from `@response_codes`.
  # - A row for CSV errors, concatenated with ":".
  #
  # @param output_file [String] The path to the output CSV file.
  # @return [void]
  def write_csv(output_file)
    CSV.open(output_file, "wb") do |csv|
      csv << ["Metric", "Value"]
      CSV_HEADER_MAPPINGS.each do |metric, value|
        csv << [metric, public_send(value)]
      end
      @response_codes.each do |code, count|
        csv << ["Response Code #{code}", count]
      end

      csv << ["CSV Errors", @csv_error_lines.join(":")]
    end
  end

  # Starts streaming and processing JTL file content asynchronously.
  # @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
  # @return [void]
  def stream_jtl_async
    @processing_jtl_thread = Thread.new do
      Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
        sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
      end

      File.open(@file_path, "r") do |file|
        until @finished && file.eof?
          line = file.gets

          # Skip if the line is nil. This can happen if not @finished, and we are at EOF
          next if line.nil?
          # Process only if the line is complete. JMeter always finishes with a newline
          read_until_complete_line(file, line)
        end
      end
    end

    @processing_jtl_thread.abort_on_exception = true
    nil
  end

  # Summarizes the collected data by calculating statistical metrics and error rates.
  #
  # @return [void]
  def summarize_data!
    @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
    @error_percentage = (@total_errors.to_f / @total_requests) * 100
    @avg = @running_statistics_helper.avg
    @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
    @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
    @standard_deviation = @running_statistics_helper.std
  end

  private

  def read_until_complete_line(file, line)
    lineno = file.lineno
    return if lineno == 1 # Skip the header row
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
      # If finished and eof but no newline: Means processing was interrupted
      # JMeter always finishes with a new line in the JTL file
      until line.end_with?("\n") || (file.eof? && @finished)
        sleep 0.1
        line += file.gets.to_s
      end
    end
    parse_csv_row(line)
  rescue Timeout::Error
    raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
  rescue CSV::MalformedCSVError
    @csv_error_lines << file.lineno
  end

  def parse_csv_row(line)
    CSV.parse(line, headers: JTL_HEADER, liberal_parsing: true).each do |row|
      line_item = row.to_hash
      elapsed = line_item.fetch(:elapsed).to_i
      timestamp = line_item.fetch(:timeStamp).to_i

      # Update start and end times
      @start_time = timestamp if @start_time.nil? || timestamp < @start_time
      @end_time = timestamp + elapsed if @end_time.nil? || (timestamp + elapsed) > @end_time

      # Continue with processing the row as before...
      @running_statistics_helper.add_number(elapsed)
      @total_requests += 1
      @response_codes[line_item.fetch(:responseCode)] += 1
      @total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
      @total_bytes += line_item.fetch(:bytes, 0).to_i
      @total_sent_bytes += line_item.fetch(:sentBytes, 0).to_i
      @total_latency += line_item.fetch(:Latency).to_i
      @min = [@min, elapsed].min
      @max = [@max, elapsed].max
    end
  end
end

#response_codesObject

Response codes have multiple keys, so we need to handle them separately



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jmeter_perf/report/summary.rb', line 53

class Summary
  # JTL file headers used for parsing CSV rows
  JTL_HEADER = %i[
    timeStamp
    elapsed
    label
    responseCode
    responseMessage
    threadName
    dataType
    success
    failureMessage
    bytes
    sentBytes
    grpThreads
    allThreads
    URL
    Latency
    IdleTime
    Connect
  ]

  # @return [Hash<String, Symbol>] a mapping of CSV headers to their corresponding attribute symbols.
  CSV_HEADER_MAPPINGS = {
    "Name" => :name,
    "Average Response Time" => :avg,
    "Error Percentage" => :error_percentage,
    "Max Response Time" => :max,
    "Min Response Time" => :min,
    "10th Percentile" => :p10,
    "Median (50th Percentile)" => :p50,
    "95th Percentile" => :p95,
    "Requests Per Minute" => :requests_per_minute,
    "Standard Deviation" => :standard_deviation,
    "Total Run Time" => :total_run_time,
    "Total Bytes" => :total_bytes,
    "Total Errors" => :total_errors,
    "Total Latency" => :total_latency,
    "Total Requests" => :total_requests,
    "Total Sent Bytes" => :total_sent_bytes
    # "Response Code 200" => :response_codes["200"],
    # "Response Code 500" => :response_codes["500"] etc.
  }

  attr_accessor(*CSV_HEADER_MAPPINGS.values)
  # Response codes have multiple keys, so we need to handle them separately
  attr_accessor :response_codes
  # CSV Error Lines are an array of integers that get delimited by ":" when written to the CSV
  attr_accessor :csv_error_lines
  alias_method :rpm, :requests_per_minute
  alias_method :std, :standard_deviation
  alias_method :median, :p50

  # Reads a generated CSV report and sets all appropriate attributes.
  #
  # @param csv_path [String] the file path of the CSV report to read
  # @return [Summary] a new Summary instance with the parsed data
  def self.read(csv_path)
    summary = new(file_path: csv_path)
    CSV.foreach(csv_path, headers: true) do |row|
      metric = row["Metric"]
      value = row["Value"]

      if metric == "Name"
        summary.name = value
      elsif metric.start_with?("Response Code")
        code = metric.split.last
        summary.response_codes[code] = value.to_i
      elsif metric == "CSV Errors"
        summary.csv_error_lines = value.split(":").map(&:to_i)
      elsif CSV_HEADER_MAPPINGS.key?(metric)
        summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
      end
    end
    summary
  end

  # Initializes a new Summary instance for analyzing performance data.
  #
  # @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
  # @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
  # @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
  def initialize(file_path:, name: nil, jtl_read_timeout: 30)
    @name = name || file_path.to_s.tr("/", "_")
    @jtl_read_timeout = jtl_read_timeout
    @finished = false
    @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

    @max = 0
    @min = 1_000_000
    @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
    @total_bytes = 0
    @total_errors = 0
    @total_latency = 0
    @total_requests = 0
    @total_sent_bytes = 0
    @csv_error_lines = []

    @file_path = file_path

    @start_time = nil
    @end_time = nil
  end

  # Marks the summary as finished and joins the processing thread.
  #
  # @return [void]
  def finish!
    @finished = true
    @processing_jtl_thread&.join
  end

  # Generates a CSV report with the given output file.
  #
  # The CSV report includes the following:
  # - A header row with "Metric" and "Value".
  # - Rows for each metric and its corresponding value from `CSV_HEADER_MAPPINGS`.
  # - Rows for each response code and its count from `@response_codes`.
  # - A row for CSV errors, concatenated with ":".
  #
  # @param output_file [String] The path to the output CSV file.
  # @return [void]
  def write_csv(output_file)
    CSV.open(output_file, "wb") do |csv|
      csv << ["Metric", "Value"]
      CSV_HEADER_MAPPINGS.each do |metric, value|
        csv << [metric, public_send(value)]
      end
      @response_codes.each do |code, count|
        csv << ["Response Code #{code}", count]
      end

      csv << ["CSV Errors", @csv_error_lines.join(":")]
    end
  end

  # Starts streaming and processing JTL file content asynchronously.
  # @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
  # @return [void]
  def stream_jtl_async
    @processing_jtl_thread = Thread.new do
      Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
        sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
      end

      File.open(@file_path, "r") do |file|
        until @finished && file.eof?
          line = file.gets

          # Skip if the line is nil. This can happen if not @finished, and we are at EOF
          next if line.nil?
          # Process only if the line is complete. JMeter always finishes with a newline
          read_until_complete_line(file, line)
        end
      end
    end

    @processing_jtl_thread.abort_on_exception = true
    nil
  end

  # Summarizes the collected data by calculating statistical metrics and error rates.
  #
  # @return [void]
  def summarize_data!
    @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
    @error_percentage = (@total_errors.to_f / @total_requests) * 100
    @avg = @running_statistics_helper.avg
    @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
    @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
    @standard_deviation = @running_statistics_helper.std
  end

  private

  def read_until_complete_line(file, line)
    lineno = file.lineno
    return if lineno == 1 # Skip the header row
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
      # If finished and eof but no newline: Means processing was interrupted
      # JMeter always finishes with a new line in the JTL file
      until line.end_with?("\n") || (file.eof? && @finished)
        sleep 0.1
        line += file.gets.to_s
      end
    end
    parse_csv_row(line)
  rescue Timeout::Error
    raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
  rescue CSV::MalformedCSVError
    @csv_error_lines << file.lineno
  end

  def parse_csv_row(line)
    CSV.parse(line, headers: JTL_HEADER, liberal_parsing: true).each do |row|
      line_item = row.to_hash
      elapsed = line_item.fetch(:elapsed).to_i
      timestamp = line_item.fetch(:timeStamp).to_i

      # Update start and end times
      @start_time = timestamp if @start_time.nil? || timestamp < @start_time
      @end_time = timestamp + elapsed if @end_time.nil? || (timestamp + elapsed) > @end_time

      # Continue with processing the row as before...
      @running_statistics_helper.add_number(elapsed)
      @total_requests += 1
      @response_codes[line_item.fetch(:responseCode)] += 1
      @total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
      @total_bytes += line_item.fetch(:bytes, 0).to_i
      @total_sent_bytes += line_item.fetch(:sentBytes, 0).to_i
      @total_latency += line_item.fetch(:Latency).to_i
      @min = [@min, elapsed].min
      @max = [@max, elapsed].max
    end
  end
end

#standard_deviationFloat Also known as: std

Returns the standard deviation of response times.

Returns:

  • (Float)

    the standard deviation of response times



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jmeter_perf/report/summary.rb', line 53

class Summary
  # JTL file headers used for parsing CSV rows
  JTL_HEADER = %i[
    timeStamp
    elapsed
    label
    responseCode
    responseMessage
    threadName
    dataType
    success
    failureMessage
    bytes
    sentBytes
    grpThreads
    allThreads
    URL
    Latency
    IdleTime
    Connect
  ]

  # @return [Hash<String, Symbol>] a mapping of CSV headers to their corresponding attribute symbols.
  CSV_HEADER_MAPPINGS = {
    "Name" => :name,
    "Average Response Time" => :avg,
    "Error Percentage" => :error_percentage,
    "Max Response Time" => :max,
    "Min Response Time" => :min,
    "10th Percentile" => :p10,
    "Median (50th Percentile)" => :p50,
    "95th Percentile" => :p95,
    "Requests Per Minute" => :requests_per_minute,
    "Standard Deviation" => :standard_deviation,
    "Total Run Time" => :total_run_time,
    "Total Bytes" => :total_bytes,
    "Total Errors" => :total_errors,
    "Total Latency" => :total_latency,
    "Total Requests" => :total_requests,
    "Total Sent Bytes" => :total_sent_bytes
    # "Response Code 200" => :response_codes["200"],
    # "Response Code 500" => :response_codes["500"] etc.
  }

  attr_accessor(*CSV_HEADER_MAPPINGS.values)
  # Response codes have multiple keys, so we need to handle them separately
  attr_accessor :response_codes
  # CSV Error Lines are an array of integers that get delimited by ":" when written to the CSV
  attr_accessor :csv_error_lines
  alias_method :rpm, :requests_per_minute
  alias_method :std, :standard_deviation
  alias_method :median, :p50

  # Reads a generated CSV report and sets all appropriate attributes.
  #
  # @param csv_path [String] the file path of the CSV report to read
  # @return [Summary] a new Summary instance with the parsed data
  def self.read(csv_path)
    summary = new(file_path: csv_path)
    CSV.foreach(csv_path, headers: true) do |row|
      metric = row["Metric"]
      value = row["Value"]

      if metric == "Name"
        summary.name = value
      elsif metric.start_with?("Response Code")
        code = metric.split.last
        summary.response_codes[code] = value.to_i
      elsif metric == "CSV Errors"
        summary.csv_error_lines = value.split(":").map(&:to_i)
      elsif CSV_HEADER_MAPPINGS.key?(metric)
        summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
      end
    end
    summary
  end

  # Initializes a new Summary instance for analyzing performance data.
  #
  # @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
  # @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
  # @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
  def initialize(file_path:, name: nil, jtl_read_timeout: 30)
    @name = name || file_path.to_s.tr("/", "_")
    @jtl_read_timeout = jtl_read_timeout
    @finished = false
    @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

    @max = 0
    @min = 1_000_000
    @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
    @total_bytes = 0
    @total_errors = 0
    @total_latency = 0
    @total_requests = 0
    @total_sent_bytes = 0
    @csv_error_lines = []

    @file_path = file_path

    @start_time = nil
    @end_time = nil
  end

  # Marks the summary as finished and joins the processing thread.
  #
  # @return [void]
  def finish!
    @finished = true
    @processing_jtl_thread&.join
  end

  # Generates a CSV report with the given output file.
  #
  # The CSV report includes the following:
  # - A header row with "Metric" and "Value".
  # - Rows for each metric and its corresponding value from `CSV_HEADER_MAPPINGS`.
  # - Rows for each response code and its count from `@response_codes`.
  # - A row for CSV errors, concatenated with ":".
  #
  # @param output_file [String] The path to the output CSV file.
  # @return [void]
  def write_csv(output_file)
    CSV.open(output_file, "wb") do |csv|
      csv << ["Metric", "Value"]
      CSV_HEADER_MAPPINGS.each do |metric, value|
        csv << [metric, public_send(value)]
      end
      @response_codes.each do |code, count|
        csv << ["Response Code #{code}", count]
      end

      csv << ["CSV Errors", @csv_error_lines.join(":")]
    end
  end

  # Starts streaming and processing JTL file content asynchronously.
  # @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
  # @return [void]
  def stream_jtl_async
    @processing_jtl_thread = Thread.new do
      Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
        sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
      end

      File.open(@file_path, "r") do |file|
        until @finished && file.eof?
          line = file.gets

          # Skip if the line is nil. This can happen if not @finished, and we are at EOF
          next if line.nil?
          # Process only if the line is complete. JMeter always finishes with a newline
          read_until_complete_line(file, line)
        end
      end
    end

    @processing_jtl_thread.abort_on_exception = true
    nil
  end

  # Summarizes the collected data by calculating statistical metrics and error rates.
  #
  # @return [void]
  def summarize_data!
    @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
    @error_percentage = (@total_errors.to_f / @total_requests) * 100
    @avg = @running_statistics_helper.avg
    @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
    @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
    @standard_deviation = @running_statistics_helper.std
  end

  private

  def read_until_complete_line(file, line)
    lineno = file.lineno
    return if lineno == 1 # Skip the header row
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
      # If finished and eof but no newline: Means processing was interrupted
      # JMeter always finishes with a new line in the JTL file
      until line.end_with?("\n") || (file.eof? && @finished)
        sleep 0.1
        line += file.gets.to_s
      end
    end
    parse_csv_row(line)
  rescue Timeout::Error
    raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
  rescue CSV::MalformedCSVError
    @csv_error_lines << file.lineno
  end

  def parse_csv_row(line)
    CSV.parse(line, headers: JTL_HEADER, liberal_parsing: true).each do |row|
      line_item = row.to_hash
      elapsed = line_item.fetch(:elapsed).to_i
      timestamp = line_item.fetch(:timeStamp).to_i

      # Update start and end times
      @start_time = timestamp if @start_time.nil? || timestamp < @start_time
      @end_time = timestamp + elapsed if @end_time.nil? || (timestamp + elapsed) > @end_time

      # Continue with processing the row as before...
      @running_statistics_helper.add_number(elapsed)
      @total_requests += 1
      @response_codes[line_item.fetch(:responseCode)] += 1
      @total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
      @total_bytes += line_item.fetch(:bytes, 0).to_i
      @total_sent_bytes += line_item.fetch(:sentBytes, 0).to_i
      @total_latency += line_item.fetch(:Latency).to_i
      @min = [@min, elapsed].min
      @max = [@max, elapsed].max
    end
  end
end

#total_bytesInteger

Returns the total number of bytes received.

Returns:

  • (Integer)

    the total number of bytes received



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jmeter_perf/report/summary.rb', line 53

class Summary
  # JTL file headers used for parsing CSV rows
  JTL_HEADER = %i[
    timeStamp
    elapsed
    label
    responseCode
    responseMessage
    threadName
    dataType
    success
    failureMessage
    bytes
    sentBytes
    grpThreads
    allThreads
    URL
    Latency
    IdleTime
    Connect
  ]

  # @return [Hash<String, Symbol>] a mapping of CSV headers to their corresponding attribute symbols.
  CSV_HEADER_MAPPINGS = {
    "Name" => :name,
    "Average Response Time" => :avg,
    "Error Percentage" => :error_percentage,
    "Max Response Time" => :max,
    "Min Response Time" => :min,
    "10th Percentile" => :p10,
    "Median (50th Percentile)" => :p50,
    "95th Percentile" => :p95,
    "Requests Per Minute" => :requests_per_minute,
    "Standard Deviation" => :standard_deviation,
    "Total Run Time" => :total_run_time,
    "Total Bytes" => :total_bytes,
    "Total Errors" => :total_errors,
    "Total Latency" => :total_latency,
    "Total Requests" => :total_requests,
    "Total Sent Bytes" => :total_sent_bytes
    # "Response Code 200" => :response_codes["200"],
    # "Response Code 500" => :response_codes["500"] etc.
  }

  attr_accessor(*CSV_HEADER_MAPPINGS.values)
  # Response codes have multiple keys, so we need to handle them separately
  attr_accessor :response_codes
  # CSV Error Lines are an array of integers that get delimited by ":" when written to the CSV
  attr_accessor :csv_error_lines
  alias_method :rpm, :requests_per_minute
  alias_method :std, :standard_deviation
  alias_method :median, :p50

  # Reads a generated CSV report and sets all appropriate attributes.
  #
  # @param csv_path [String] the file path of the CSV report to read
  # @return [Summary] a new Summary instance with the parsed data
  def self.read(csv_path)
    summary = new(file_path: csv_path)
    CSV.foreach(csv_path, headers: true) do |row|
      metric = row["Metric"]
      value = row["Value"]

      if metric == "Name"
        summary.name = value
      elsif metric.start_with?("Response Code")
        code = metric.split.last
        summary.response_codes[code] = value.to_i
      elsif metric == "CSV Errors"
        summary.csv_error_lines = value.split(":").map(&:to_i)
      elsif CSV_HEADER_MAPPINGS.key?(metric)
        summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
      end
    end
    summary
  end

  # Initializes a new Summary instance for analyzing performance data.
  #
  # @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
  # @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
  # @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
  def initialize(file_path:, name: nil, jtl_read_timeout: 30)
    @name = name || file_path.to_s.tr("/", "_")
    @jtl_read_timeout = jtl_read_timeout
    @finished = false
    @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

    @max = 0
    @min = 1_000_000
    @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
    @total_bytes = 0
    @total_errors = 0
    @total_latency = 0
    @total_requests = 0
    @total_sent_bytes = 0
    @csv_error_lines = []

    @file_path = file_path

    @start_time = nil
    @end_time = nil
  end

  # Marks the summary as finished and joins the processing thread.
  #
  # @return [void]
  def finish!
    @finished = true
    @processing_jtl_thread&.join
  end

  # Generates a CSV report with the given output file.
  #
  # The CSV report includes the following:
  # - A header row with "Metric" and "Value".
  # - Rows for each metric and its corresponding value from `CSV_HEADER_MAPPINGS`.
  # - Rows for each response code and its count from `@response_codes`.
  # - A row for CSV errors, concatenated with ":".
  #
  # @param output_file [String] The path to the output CSV file.
  # @return [void]
  def write_csv(output_file)
    CSV.open(output_file, "wb") do |csv|
      csv << ["Metric", "Value"]
      CSV_HEADER_MAPPINGS.each do |metric, value|
        csv << [metric, public_send(value)]
      end
      @response_codes.each do |code, count|
        csv << ["Response Code #{code}", count]
      end

      csv << ["CSV Errors", @csv_error_lines.join(":")]
    end
  end

  # Starts streaming and processing JTL file content asynchronously.
  # @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
  # @return [void]
  def stream_jtl_async
    @processing_jtl_thread = Thread.new do
      Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
        sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
      end

      File.open(@file_path, "r") do |file|
        until @finished && file.eof?
          line = file.gets

          # Skip if the line is nil. This can happen if not @finished, and we are at EOF
          next if line.nil?
          # Process only if the line is complete. JMeter always finishes with a newline
          read_until_complete_line(file, line)
        end
      end
    end

    @processing_jtl_thread.abort_on_exception = true
    nil
  end

  # Summarizes the collected data by calculating statistical metrics and error rates.
  #
  # @return [void]
  def summarize_data!
    @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
    @error_percentage = (@total_errors.to_f / @total_requests) * 100
    @avg = @running_statistics_helper.avg
    @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
    @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
    @standard_deviation = @running_statistics_helper.std
  end

  private

  def read_until_complete_line(file, line)
    lineno = file.lineno
    return if lineno == 1 # Skip the header row
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
      # If finished and eof but no newline: Means processing was interrupted
      # JMeter always finishes with a new line in the JTL file
      until line.end_with?("\n") || (file.eof? && @finished)
        sleep 0.1
        line += file.gets.to_s
      end
    end
    parse_csv_row(line)
  rescue Timeout::Error
    raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
  rescue CSV::MalformedCSVError
    @csv_error_lines << file.lineno
  end

  def parse_csv_row(line)
    CSV.parse(line, headers: JTL_HEADER, liberal_parsing: true).each do |row|
      line_item = row.to_hash
      elapsed = line_item.fetch(:elapsed).to_i
      timestamp = line_item.fetch(:timeStamp).to_i

      # Update start and end times
      @start_time = timestamp if @start_time.nil? || timestamp < @start_time
      @end_time = timestamp + elapsed if @end_time.nil? || (timestamp + elapsed) > @end_time

      # Continue with processing the row as before...
      @running_statistics_helper.add_number(elapsed)
      @total_requests += 1
      @response_codes[line_item.fetch(:responseCode)] += 1
      @total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
      @total_bytes += line_item.fetch(:bytes, 0).to_i
      @total_sent_bytes += line_item.fetch(:sentBytes, 0).to_i
      @total_latency += line_item.fetch(:Latency).to_i
      @min = [@min, elapsed].min
      @max = [@max, elapsed].max
    end
  end
end

#total_errorsInteger

Returns the total number of errors encountered.

Returns:

  • (Integer)

    the total number of errors encountered



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jmeter_perf/report/summary.rb', line 53

class Summary
  # JTL file headers used for parsing CSV rows
  JTL_HEADER = %i[
    timeStamp
    elapsed
    label
    responseCode
    responseMessage
    threadName
    dataType
    success
    failureMessage
    bytes
    sentBytes
    grpThreads
    allThreads
    URL
    Latency
    IdleTime
    Connect
  ]

  # @return [Hash<String, Symbol>] a mapping of CSV headers to their corresponding attribute symbols.
  CSV_HEADER_MAPPINGS = {
    "Name" => :name,
    "Average Response Time" => :avg,
    "Error Percentage" => :error_percentage,
    "Max Response Time" => :max,
    "Min Response Time" => :min,
    "10th Percentile" => :p10,
    "Median (50th Percentile)" => :p50,
    "95th Percentile" => :p95,
    "Requests Per Minute" => :requests_per_minute,
    "Standard Deviation" => :standard_deviation,
    "Total Run Time" => :total_run_time,
    "Total Bytes" => :total_bytes,
    "Total Errors" => :total_errors,
    "Total Latency" => :total_latency,
    "Total Requests" => :total_requests,
    "Total Sent Bytes" => :total_sent_bytes
    # "Response Code 200" => :response_codes["200"],
    # "Response Code 500" => :response_codes["500"] etc.
  }

  attr_accessor(*CSV_HEADER_MAPPINGS.values)
  # Response codes have multiple keys, so we need to handle them separately
  attr_accessor :response_codes
  # CSV Error Lines are an array of integers that get delimited by ":" when written to the CSV
  attr_accessor :csv_error_lines
  alias_method :rpm, :requests_per_minute
  alias_method :std, :standard_deviation
  alias_method :median, :p50

  # Reads a generated CSV report and sets all appropriate attributes.
  #
  # @param csv_path [String] the file path of the CSV report to read
  # @return [Summary] a new Summary instance with the parsed data
  def self.read(csv_path)
    summary = new(file_path: csv_path)
    CSV.foreach(csv_path, headers: true) do |row|
      metric = row["Metric"]
      value = row["Value"]

      if metric == "Name"
        summary.name = value
      elsif metric.start_with?("Response Code")
        code = metric.split.last
        summary.response_codes[code] = value.to_i
      elsif metric == "CSV Errors"
        summary.csv_error_lines = value.split(":").map(&:to_i)
      elsif CSV_HEADER_MAPPINGS.key?(metric)
        summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
      end
    end
    summary
  end

  # Initializes a new Summary instance for analyzing performance data.
  #
  # @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
  # @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
  # @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
  def initialize(file_path:, name: nil, jtl_read_timeout: 30)
    @name = name || file_path.to_s.tr("/", "_")
    @jtl_read_timeout = jtl_read_timeout
    @finished = false
    @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

    @max = 0
    @min = 1_000_000
    @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
    @total_bytes = 0
    @total_errors = 0
    @total_latency = 0
    @total_requests = 0
    @total_sent_bytes = 0
    @csv_error_lines = []

    @file_path = file_path

    @start_time = nil
    @end_time = nil
  end

  # Marks the summary as finished and joins the processing thread.
  #
  # @return [void]
  def finish!
    @finished = true
    @processing_jtl_thread&.join
  end

  # Generates a CSV report with the given output file.
  #
  # The CSV report includes the following:
  # - A header row with "Metric" and "Value".
  # - Rows for each metric and its corresponding value from `CSV_HEADER_MAPPINGS`.
  # - Rows for each response code and its count from `@response_codes`.
  # - A row for CSV errors, concatenated with ":".
  #
  # @param output_file [String] The path to the output CSV file.
  # @return [void]
  def write_csv(output_file)
    CSV.open(output_file, "wb") do |csv|
      csv << ["Metric", "Value"]
      CSV_HEADER_MAPPINGS.each do |metric, value|
        csv << [metric, public_send(value)]
      end
      @response_codes.each do |code, count|
        csv << ["Response Code #{code}", count]
      end

      csv << ["CSV Errors", @csv_error_lines.join(":")]
    end
  end

  # Starts streaming and processing JTL file content asynchronously.
  # @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
  # @return [void]
  def stream_jtl_async
    @processing_jtl_thread = Thread.new do
      Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
        sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
      end

      File.open(@file_path, "r") do |file|
        until @finished && file.eof?
          line = file.gets

          # Skip if the line is nil. This can happen if not @finished, and we are at EOF
          next if line.nil?
          # Process only if the line is complete. JMeter always finishes with a newline
          read_until_complete_line(file, line)
        end
      end
    end

    @processing_jtl_thread.abort_on_exception = true
    nil
  end

  # Summarizes the collected data by calculating statistical metrics and error rates.
  #
  # @return [void]
  def summarize_data!
    @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
    @error_percentage = (@total_errors.to_f / @total_requests) * 100
    @avg = @running_statistics_helper.avg
    @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
    @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
    @standard_deviation = @running_statistics_helper.std
  end

  private

  def read_until_complete_line(file, line)
    lineno = file.lineno
    return if lineno == 1 # Skip the header row
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
      # If finished and eof but no newline: Means processing was interrupted
      # JMeter always finishes with a new line in the JTL file
      until line.end_with?("\n") || (file.eof? && @finished)
        sleep 0.1
        line += file.gets.to_s
      end
    end
    parse_csv_row(line)
  rescue Timeout::Error
    raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
  rescue CSV::MalformedCSVError
    @csv_error_lines << file.lineno
  end

  def parse_csv_row(line)
    CSV.parse(line, headers: JTL_HEADER, liberal_parsing: true).each do |row|
      line_item = row.to_hash
      elapsed = line_item.fetch(:elapsed).to_i
      timestamp = line_item.fetch(:timeStamp).to_i

      # Update start and end times
      @start_time = timestamp if @start_time.nil? || timestamp < @start_time
      @end_time = timestamp + elapsed if @end_time.nil? || (timestamp + elapsed) > @end_time

      # Continue with processing the row as before...
      @running_statistics_helper.add_number(elapsed)
      @total_requests += 1
      @response_codes[line_item.fetch(:responseCode)] += 1
      @total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
      @total_bytes += line_item.fetch(:bytes, 0).to_i
      @total_sent_bytes += line_item.fetch(:sentBytes, 0).to_i
      @total_latency += line_item.fetch(:Latency).to_i
      @min = [@min, elapsed].min
      @max = [@max, elapsed].max
    end
  end
end

#total_latencyInteger

Returns the total latency time across all requests.

Returns:

  • (Integer)

    the total latency time across all requests



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jmeter_perf/report/summary.rb', line 53

class Summary
  # JTL file headers used for parsing CSV rows
  JTL_HEADER = %i[
    timeStamp
    elapsed
    label
    responseCode
    responseMessage
    threadName
    dataType
    success
    failureMessage
    bytes
    sentBytes
    grpThreads
    allThreads
    URL
    Latency
    IdleTime
    Connect
  ]

  # @return [Hash<String, Symbol>] a mapping of CSV headers to their corresponding attribute symbols.
  CSV_HEADER_MAPPINGS = {
    "Name" => :name,
    "Average Response Time" => :avg,
    "Error Percentage" => :error_percentage,
    "Max Response Time" => :max,
    "Min Response Time" => :min,
    "10th Percentile" => :p10,
    "Median (50th Percentile)" => :p50,
    "95th Percentile" => :p95,
    "Requests Per Minute" => :requests_per_minute,
    "Standard Deviation" => :standard_deviation,
    "Total Run Time" => :total_run_time,
    "Total Bytes" => :total_bytes,
    "Total Errors" => :total_errors,
    "Total Latency" => :total_latency,
    "Total Requests" => :total_requests,
    "Total Sent Bytes" => :total_sent_bytes
    # "Response Code 200" => :response_codes["200"],
    # "Response Code 500" => :response_codes["500"] etc.
  }

  attr_accessor(*CSV_HEADER_MAPPINGS.values)
  # Response codes have multiple keys, so we need to handle them separately
  attr_accessor :response_codes
  # CSV Error Lines are an array of integers that get delimited by ":" when written to the CSV
  attr_accessor :csv_error_lines
  alias_method :rpm, :requests_per_minute
  alias_method :std, :standard_deviation
  alias_method :median, :p50

  # Reads a generated CSV report and sets all appropriate attributes.
  #
  # @param csv_path [String] the file path of the CSV report to read
  # @return [Summary] a new Summary instance with the parsed data
  def self.read(csv_path)
    summary = new(file_path: csv_path)
    CSV.foreach(csv_path, headers: true) do |row|
      metric = row["Metric"]
      value = row["Value"]

      if metric == "Name"
        summary.name = value
      elsif metric.start_with?("Response Code")
        code = metric.split.last
        summary.response_codes[code] = value.to_i
      elsif metric == "CSV Errors"
        summary.csv_error_lines = value.split(":").map(&:to_i)
      elsif CSV_HEADER_MAPPINGS.key?(metric)
        summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
      end
    end
    summary
  end

  # Initializes a new Summary instance for analyzing performance data.
  #
  # @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
  # @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
  # @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
  def initialize(file_path:, name: nil, jtl_read_timeout: 30)
    @name = name || file_path.to_s.tr("/", "_")
    @jtl_read_timeout = jtl_read_timeout
    @finished = false
    @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

    @max = 0
    @min = 1_000_000
    @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
    @total_bytes = 0
    @total_errors = 0
    @total_latency = 0
    @total_requests = 0
    @total_sent_bytes = 0
    @csv_error_lines = []

    @file_path = file_path

    @start_time = nil
    @end_time = nil
  end

  # Marks the summary as finished and joins the processing thread.
  #
  # @return [void]
  def finish!
    @finished = true
    @processing_jtl_thread&.join
  end

  # Generates a CSV report with the given output file.
  #
  # The CSV report includes the following:
  # - A header row with "Metric" and "Value".
  # - Rows for each metric and its corresponding value from `CSV_HEADER_MAPPINGS`.
  # - Rows for each response code and its count from `@response_codes`.
  # - A row for CSV errors, concatenated with ":".
  #
  # @param output_file [String] The path to the output CSV file.
  # @return [void]
  def write_csv(output_file)
    CSV.open(output_file, "wb") do |csv|
      csv << ["Metric", "Value"]
      CSV_HEADER_MAPPINGS.each do |metric, value|
        csv << [metric, public_send(value)]
      end
      @response_codes.each do |code, count|
        csv << ["Response Code #{code}", count]
      end

      csv << ["CSV Errors", @csv_error_lines.join(":")]
    end
  end

  # Starts streaming and processing JTL file content asynchronously.
  # @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
  # @return [void]
  def stream_jtl_async
    @processing_jtl_thread = Thread.new do
      Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
        sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
      end

      File.open(@file_path, "r") do |file|
        until @finished && file.eof?
          line = file.gets

          # Skip if the line is nil. This can happen if not @finished, and we are at EOF
          next if line.nil?
          # Process only if the line is complete. JMeter always finishes with a newline
          read_until_complete_line(file, line)
        end
      end
    end

    @processing_jtl_thread.abort_on_exception = true
    nil
  end

  # Summarizes the collected data by calculating statistical metrics and error rates.
  #
  # @return [void]
  def summarize_data!
    @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
    @error_percentage = (@total_errors.to_f / @total_requests) * 100
    @avg = @running_statistics_helper.avg
    @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
    @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
    @standard_deviation = @running_statistics_helper.std
  end

  private

  def read_until_complete_line(file, line)
    lineno = file.lineno
    return if lineno == 1 # Skip the header row
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
      # If finished and eof but no newline: Means processing was interrupted
      # JMeter always finishes with a new line in the JTL file
      until line.end_with?("\n") || (file.eof? && @finished)
        sleep 0.1
        line += file.gets.to_s
      end
    end
    parse_csv_row(line)
  rescue Timeout::Error
    raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
  rescue CSV::MalformedCSVError
    @csv_error_lines << file.lineno
  end

  def parse_csv_row(line)
    CSV.parse(line, headers: JTL_HEADER, liberal_parsing: true).each do |row|
      line_item = row.to_hash
      elapsed = line_item.fetch(:elapsed).to_i
      timestamp = line_item.fetch(:timeStamp).to_i

      # Update start and end times
      @start_time = timestamp if @start_time.nil? || timestamp < @start_time
      @end_time = timestamp + elapsed if @end_time.nil? || (timestamp + elapsed) > @end_time

      # Continue with processing the row as before...
      @running_statistics_helper.add_number(elapsed)
      @total_requests += 1
      @response_codes[line_item.fetch(:responseCode)] += 1
      @total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
      @total_bytes += line_item.fetch(:bytes, 0).to_i
      @total_sent_bytes += line_item.fetch(:sentBytes, 0).to_i
      @total_latency += line_item.fetch(:Latency).to_i
      @min = [@min, elapsed].min
      @max = [@max, elapsed].max
    end
  end
end

#total_requestsInteger

Returns the total number of requests processed.

Returns:

  • (Integer)

    the total number of requests processed



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jmeter_perf/report/summary.rb', line 53

class Summary
  # JTL file headers used for parsing CSV rows
  JTL_HEADER = %i[
    timeStamp
    elapsed
    label
    responseCode
    responseMessage
    threadName
    dataType
    success
    failureMessage
    bytes
    sentBytes
    grpThreads
    allThreads
    URL
    Latency
    IdleTime
    Connect
  ]

  # @return [Hash<String, Symbol>] a mapping of CSV headers to their corresponding attribute symbols.
  CSV_HEADER_MAPPINGS = {
    "Name" => :name,
    "Average Response Time" => :avg,
    "Error Percentage" => :error_percentage,
    "Max Response Time" => :max,
    "Min Response Time" => :min,
    "10th Percentile" => :p10,
    "Median (50th Percentile)" => :p50,
    "95th Percentile" => :p95,
    "Requests Per Minute" => :requests_per_minute,
    "Standard Deviation" => :standard_deviation,
    "Total Run Time" => :total_run_time,
    "Total Bytes" => :total_bytes,
    "Total Errors" => :total_errors,
    "Total Latency" => :total_latency,
    "Total Requests" => :total_requests,
    "Total Sent Bytes" => :total_sent_bytes
    # "Response Code 200" => :response_codes["200"],
    # "Response Code 500" => :response_codes["500"] etc.
  }

  attr_accessor(*CSV_HEADER_MAPPINGS.values)
  # Response codes have multiple keys, so we need to handle them separately
  attr_accessor :response_codes
  # CSV Error Lines are an array of integers that get delimited by ":" when written to the CSV
  attr_accessor :csv_error_lines
  alias_method :rpm, :requests_per_minute
  alias_method :std, :standard_deviation
  alias_method :median, :p50

  # Reads a generated CSV report and sets all appropriate attributes.
  #
  # @param csv_path [String] the file path of the CSV report to read
  # @return [Summary] a new Summary instance with the parsed data
  def self.read(csv_path)
    summary = new(file_path: csv_path)
    CSV.foreach(csv_path, headers: true) do |row|
      metric = row["Metric"]
      value = row["Value"]

      if metric == "Name"
        summary.name = value
      elsif metric.start_with?("Response Code")
        code = metric.split.last
        summary.response_codes[code] = value.to_i
      elsif metric == "CSV Errors"
        summary.csv_error_lines = value.split(":").map(&:to_i)
      elsif CSV_HEADER_MAPPINGS.key?(metric)
        summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
      end
    end
    summary
  end

  # Initializes a new Summary instance for analyzing performance data.
  #
  # @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
  # @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
  # @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
  def initialize(file_path:, name: nil, jtl_read_timeout: 30)
    @name = name || file_path.to_s.tr("/", "_")
    @jtl_read_timeout = jtl_read_timeout
    @finished = false
    @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

    @max = 0
    @min = 1_000_000
    @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
    @total_bytes = 0
    @total_errors = 0
    @total_latency = 0
    @total_requests = 0
    @total_sent_bytes = 0
    @csv_error_lines = []

    @file_path = file_path

    @start_time = nil
    @end_time = nil
  end

  # Marks the summary as finished and joins the processing thread.
  #
  # @return [void]
  def finish!
    @finished = true
    @processing_jtl_thread&.join
  end

  # Generates a CSV report with the given output file.
  #
  # The CSV report includes the following:
  # - A header row with "Metric" and "Value".
  # - Rows for each metric and its corresponding value from `CSV_HEADER_MAPPINGS`.
  # - Rows for each response code and its count from `@response_codes`.
  # - A row for CSV errors, concatenated with ":".
  #
  # @param output_file [String] The path to the output CSV file.
  # @return [void]
  def write_csv(output_file)
    CSV.open(output_file, "wb") do |csv|
      csv << ["Metric", "Value"]
      CSV_HEADER_MAPPINGS.each do |metric, value|
        csv << [metric, public_send(value)]
      end
      @response_codes.each do |code, count|
        csv << ["Response Code #{code}", count]
      end

      csv << ["CSV Errors", @csv_error_lines.join(":")]
    end
  end

  # Starts streaming and processing JTL file content asynchronously.
  # @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
  # @return [void]
  def stream_jtl_async
    @processing_jtl_thread = Thread.new do
      Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
        sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
      end

      File.open(@file_path, "r") do |file|
        until @finished && file.eof?
          line = file.gets

          # Skip if the line is nil. This can happen if not @finished, and we are at EOF
          next if line.nil?
          # Process only if the line is complete. JMeter always finishes with a newline
          read_until_complete_line(file, line)
        end
      end
    end

    @processing_jtl_thread.abort_on_exception = true
    nil
  end

  # Summarizes the collected data by calculating statistical metrics and error rates.
  #
  # @return [void]
  def summarize_data!
    @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
    @error_percentage = (@total_errors.to_f / @total_requests) * 100
    @avg = @running_statistics_helper.avg
    @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
    @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
    @standard_deviation = @running_statistics_helper.std
  end

  private

  def read_until_complete_line(file, line)
    lineno = file.lineno
    return if lineno == 1 # Skip the header row
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
      # If finished and eof but no newline: Means processing was interrupted
      # JMeter always finishes with a new line in the JTL file
      until line.end_with?("\n") || (file.eof? && @finished)
        sleep 0.1
        line += file.gets.to_s
      end
    end
    parse_csv_row(line)
  rescue Timeout::Error
    raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
  rescue CSV::MalformedCSVError
    @csv_error_lines << file.lineno
  end

  def parse_csv_row(line)
    CSV.parse(line, headers: JTL_HEADER, liberal_parsing: true).each do |row|
      line_item = row.to_hash
      elapsed = line_item.fetch(:elapsed).to_i
      timestamp = line_item.fetch(:timeStamp).to_i

      # Update start and end times
      @start_time = timestamp if @start_time.nil? || timestamp < @start_time
      @end_time = timestamp + elapsed if @end_time.nil? || (timestamp + elapsed) > @end_time

      # Continue with processing the row as before...
      @running_statistics_helper.add_number(elapsed)
      @total_requests += 1
      @response_codes[line_item.fetch(:responseCode)] += 1
      @total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
      @total_bytes += line_item.fetch(:bytes, 0).to_i
      @total_sent_bytes += line_item.fetch(:sentBytes, 0).to_i
      @total_latency += line_item.fetch(:Latency).to_i
      @min = [@min, elapsed].min
      @max = [@max, elapsed].max
    end
  end
end

#total_run_timeInteger

Returns the total run time in seconds.

Returns:

  • (Integer)

    the total run time in seconds



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jmeter_perf/report/summary.rb', line 53

class Summary
  # JTL file headers used for parsing CSV rows
  JTL_HEADER = %i[
    timeStamp
    elapsed
    label
    responseCode
    responseMessage
    threadName
    dataType
    success
    failureMessage
    bytes
    sentBytes
    grpThreads
    allThreads
    URL
    Latency
    IdleTime
    Connect
  ]

  # @return [Hash<String, Symbol>] a mapping of CSV headers to their corresponding attribute symbols.
  CSV_HEADER_MAPPINGS = {
    "Name" => :name,
    "Average Response Time" => :avg,
    "Error Percentage" => :error_percentage,
    "Max Response Time" => :max,
    "Min Response Time" => :min,
    "10th Percentile" => :p10,
    "Median (50th Percentile)" => :p50,
    "95th Percentile" => :p95,
    "Requests Per Minute" => :requests_per_minute,
    "Standard Deviation" => :standard_deviation,
    "Total Run Time" => :total_run_time,
    "Total Bytes" => :total_bytes,
    "Total Errors" => :total_errors,
    "Total Latency" => :total_latency,
    "Total Requests" => :total_requests,
    "Total Sent Bytes" => :total_sent_bytes
    # "Response Code 200" => :response_codes["200"],
    # "Response Code 500" => :response_codes["500"] etc.
  }

  attr_accessor(*CSV_HEADER_MAPPINGS.values)
  # Response codes have multiple keys, so we need to handle them separately
  attr_accessor :response_codes
  # CSV Error Lines are an array of integers that get delimited by ":" when written to the CSV
  attr_accessor :csv_error_lines
  alias_method :rpm, :requests_per_minute
  alias_method :std, :standard_deviation
  alias_method :median, :p50

  # Reads a generated CSV report and sets all appropriate attributes.
  #
  # @param csv_path [String] the file path of the CSV report to read
  # @return [Summary] a new Summary instance with the parsed data
  def self.read(csv_path)
    summary = new(file_path: csv_path)
    CSV.foreach(csv_path, headers: true) do |row|
      metric = row["Metric"]
      value = row["Value"]

      if metric == "Name"
        summary.name = value
      elsif metric.start_with?("Response Code")
        code = metric.split.last
        summary.response_codes[code] = value.to_i
      elsif metric == "CSV Errors"
        summary.csv_error_lines = value.split(":").map(&:to_i)
      elsif CSV_HEADER_MAPPINGS.key?(metric)
        summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
      end
    end
    summary
  end

  # Initializes a new Summary instance for analyzing performance data.
  #
  # @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
  # @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
  # @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
  def initialize(file_path:, name: nil, jtl_read_timeout: 30)
    @name = name || file_path.to_s.tr("/", "_")
    @jtl_read_timeout = jtl_read_timeout
    @finished = false
    @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

    @max = 0
    @min = 1_000_000
    @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
    @total_bytes = 0
    @total_errors = 0
    @total_latency = 0
    @total_requests = 0
    @total_sent_bytes = 0
    @csv_error_lines = []

    @file_path = file_path

    @start_time = nil
    @end_time = nil
  end

  # Marks the summary as finished and joins the processing thread.
  #
  # @return [void]
  def finish!
    @finished = true
    @processing_jtl_thread&.join
  end

  # Generates a CSV report with the given output file.
  #
  # The CSV report includes the following:
  # - A header row with "Metric" and "Value".
  # - Rows for each metric and its corresponding value from `CSV_HEADER_MAPPINGS`.
  # - Rows for each response code and its count from `@response_codes`.
  # - A row for CSV errors, concatenated with ":".
  #
  # @param output_file [String] The path to the output CSV file.
  # @return [void]
  def write_csv(output_file)
    CSV.open(output_file, "wb") do |csv|
      csv << ["Metric", "Value"]
      CSV_HEADER_MAPPINGS.each do |metric, value|
        csv << [metric, public_send(value)]
      end
      @response_codes.each do |code, count|
        csv << ["Response Code #{code}", count]
      end

      csv << ["CSV Errors", @csv_error_lines.join(":")]
    end
  end

  # Starts streaming and processing JTL file content asynchronously.
  # @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
  # @return [void]
  def stream_jtl_async
    @processing_jtl_thread = Thread.new do
      Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
        sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
      end

      File.open(@file_path, "r") do |file|
        until @finished && file.eof?
          line = file.gets

          # Skip if the line is nil. This can happen if not @finished, and we are at EOF
          next if line.nil?
          # Process only if the line is complete. JMeter always finishes with a newline
          read_until_complete_line(file, line)
        end
      end
    end

    @processing_jtl_thread.abort_on_exception = true
    nil
  end

  # Summarizes the collected data by calculating statistical metrics and error rates.
  #
  # @return [void]
  def summarize_data!
    @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
    @error_percentage = (@total_errors.to_f / @total_requests) * 100
    @avg = @running_statistics_helper.avg
    @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
    @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
    @standard_deviation = @running_statistics_helper.std
  end

  private

  def read_until_complete_line(file, line)
    lineno = file.lineno
    return if lineno == 1 # Skip the header row
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
      # If finished and eof but no newline: Means processing was interrupted
      # JMeter always finishes with a new line in the JTL file
      until line.end_with?("\n") || (file.eof? && @finished)
        sleep 0.1
        line += file.gets.to_s
      end
    end
    parse_csv_row(line)
  rescue Timeout::Error
    raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
  rescue CSV::MalformedCSVError
    @csv_error_lines << file.lineno
  end

  def parse_csv_row(line)
    CSV.parse(line, headers: JTL_HEADER, liberal_parsing: true).each do |row|
      line_item = row.to_hash
      elapsed = line_item.fetch(:elapsed).to_i
      timestamp = line_item.fetch(:timeStamp).to_i

      # Update start and end times
      @start_time = timestamp if @start_time.nil? || timestamp < @start_time
      @end_time = timestamp + elapsed if @end_time.nil? || (timestamp + elapsed) > @end_time

      # Continue with processing the row as before...
      @running_statistics_helper.add_number(elapsed)
      @total_requests += 1
      @response_codes[line_item.fetch(:responseCode)] += 1
      @total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
      @total_bytes += line_item.fetch(:bytes, 0).to_i
      @total_sent_bytes += line_item.fetch(:sentBytes, 0).to_i
      @total_latency += line_item.fetch(:Latency).to_i
      @min = [@min, elapsed].min
      @max = [@max, elapsed].max
    end
  end
end

#total_sent_bytesInteger

Returns the total number of bytes sent.

Returns:

  • (Integer)

    the total number of bytes sent



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/jmeter_perf/report/summary.rb', line 53

class Summary
  # JTL file headers used for parsing CSV rows
  JTL_HEADER = %i[
    timeStamp
    elapsed
    label
    responseCode
    responseMessage
    threadName
    dataType
    success
    failureMessage
    bytes
    sentBytes
    grpThreads
    allThreads
    URL
    Latency
    IdleTime
    Connect
  ]

  # @return [Hash<String, Symbol>] a mapping of CSV headers to their corresponding attribute symbols.
  CSV_HEADER_MAPPINGS = {
    "Name" => :name,
    "Average Response Time" => :avg,
    "Error Percentage" => :error_percentage,
    "Max Response Time" => :max,
    "Min Response Time" => :min,
    "10th Percentile" => :p10,
    "Median (50th Percentile)" => :p50,
    "95th Percentile" => :p95,
    "Requests Per Minute" => :requests_per_minute,
    "Standard Deviation" => :standard_deviation,
    "Total Run Time" => :total_run_time,
    "Total Bytes" => :total_bytes,
    "Total Errors" => :total_errors,
    "Total Latency" => :total_latency,
    "Total Requests" => :total_requests,
    "Total Sent Bytes" => :total_sent_bytes
    # "Response Code 200" => :response_codes["200"],
    # "Response Code 500" => :response_codes["500"] etc.
  }

  attr_accessor(*CSV_HEADER_MAPPINGS.values)
  # Response codes have multiple keys, so we need to handle them separately
  attr_accessor :response_codes
  # CSV Error Lines are an array of integers that get delimited by ":" when written to the CSV
  attr_accessor :csv_error_lines
  alias_method :rpm, :requests_per_minute
  alias_method :std, :standard_deviation
  alias_method :median, :p50

  # Reads a generated CSV report and sets all appropriate attributes.
  #
  # @param csv_path [String] the file path of the CSV report to read
  # @return [Summary] a new Summary instance with the parsed data
  def self.read(csv_path)
    summary = new(file_path: csv_path)
    CSV.foreach(csv_path, headers: true) do |row|
      metric = row["Metric"]
      value = row["Value"]

      if metric == "Name"
        summary.name = value
      elsif metric.start_with?("Response Code")
        code = metric.split.last
        summary.response_codes[code] = value.to_i
      elsif metric == "CSV Errors"
        summary.csv_error_lines = value.split(":").map(&:to_i)
      elsif CSV_HEADER_MAPPINGS.key?(metric)
        summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
      end
    end
    summary
  end

  # Initializes a new Summary instance for analyzing performance data.
  #
  # @param file_path [String] the file path of the performance file to summarize. Either a JTL or CSV file.
  # @param name [String, nil] an optional name for the summary, derived from the file path if not provided (default: nil)
  # @param jtl_read_timeout [Integer] the maximum number of seconds to wait for a line read (default: 3)
  def initialize(file_path:, name: nil, jtl_read_timeout: 30)
    @name = name || file_path.to_s.tr("/", "_")
    @jtl_read_timeout = jtl_read_timeout
    @finished = false
    @running_statistics_helper = JmeterPerf::Helpers::RunningStatistisc.new

    @max = 0
    @min = 1_000_000
    @response_codes = Hash.new { |h, k| h[k.to_s] = 0 }
    @total_bytes = 0
    @total_errors = 0
    @total_latency = 0
    @total_requests = 0
    @total_sent_bytes = 0
    @csv_error_lines = []

    @file_path = file_path

    @start_time = nil
    @end_time = nil
  end

  # Marks the summary as finished and joins the processing thread.
  #
  # @return [void]
  def finish!
    @finished = true
    @processing_jtl_thread&.join
  end

  # Generates a CSV report with the given output file.
  #
  # The CSV report includes the following:
  # - A header row with "Metric" and "Value".
  # - Rows for each metric and its corresponding value from `CSV_HEADER_MAPPINGS`.
  # - Rows for each response code and its count from `@response_codes`.
  # - A row for CSV errors, concatenated with ":".
  #
  # @param output_file [String] The path to the output CSV file.
  # @return [void]
  def write_csv(output_file)
    CSV.open(output_file, "wb") do |csv|
      csv << ["Metric", "Value"]
      CSV_HEADER_MAPPINGS.each do |metric, value|
        csv << [metric, public_send(value)]
      end
      @response_codes.each do |code, count|
        csv << ["Response Code #{code}", count]
      end

      csv << ["CSV Errors", @csv_error_lines.join(":")]
    end
  end

  # Starts streaming and processing JTL file content asynchronously.
  # @note Once streaming, in order to finish processing, call `finish!` otherwise it will continue indefinitely.
  # @return [void]
  def stream_jtl_async
    @processing_jtl_thread = Thread.new do
      Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
        sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
      end

      File.open(@file_path, "r") do |file|
        until @finished && file.eof?
          line = file.gets

          # Skip if the line is nil. This can happen if not @finished, and we are at EOF
          next if line.nil?
          # Process only if the line is complete. JMeter always finishes with a newline
          read_until_complete_line(file, line)
        end
      end
    end

    @processing_jtl_thread.abort_on_exception = true
    nil
  end

  # Summarizes the collected data by calculating statistical metrics and error rates.
  #
  # @return [void]
  def summarize_data!
    @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
    @error_percentage = (@total_errors.to_f / @total_requests) * 100
    @avg = @running_statistics_helper.avg
    @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
    @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
    @standard_deviation = @running_statistics_helper.std
  end

  private

  def read_until_complete_line(file, line)
    lineno = file.lineno
    return if lineno == 1 # Skip the header row
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out processing line #{lineno}") do
      # If finished and eof but no newline: Means processing was interrupted
      # JMeter always finishes with a new line in the JTL file
      until line.end_with?("\n") || (file.eof? && @finished)
        sleep 0.1
        line += file.gets.to_s
      end
    end
    parse_csv_row(line)
  rescue Timeout::Error
    raise Timeout::Error, "Timed out reading JTL file at line #{lineno}"
  rescue CSV::MalformedCSVError
    @csv_error_lines << file.lineno
  end

  def parse_csv_row(line)
    CSV.parse(line, headers: JTL_HEADER, liberal_parsing: true).each do |row|
      line_item = row.to_hash
      elapsed = line_item.fetch(:elapsed).to_i
      timestamp = line_item.fetch(:timeStamp).to_i

      # Update start and end times
      @start_time = timestamp if @start_time.nil? || timestamp < @start_time
      @end_time = timestamp + elapsed if @end_time.nil? || (timestamp + elapsed) > @end_time

      # Continue with processing the row as before...
      @running_statistics_helper.add_number(elapsed)
      @total_requests += 1
      @response_codes[line_item.fetch(:responseCode)] += 1
      @total_errors += (line_item.fetch(:success) == "true") ? 0 : 1
      @total_bytes += line_item.fetch(:bytes, 0).to_i
      @total_sent_bytes += line_item.fetch(:sentBytes, 0).to_i
      @total_latency += line_item.fetch(:Latency).to_i
      @min = [@min, elapsed].min
      @max = [@max, elapsed].max
    end
  end
end

Class Method Details

.read(csv_path) ⇒ Summary

Reads a generated CSV report and sets all appropriate attributes.

Parameters:

  • csv_path (String)

    the file path of the CSV report to read

Returns:

  • (Summary)

    a new Summary instance with the parsed data



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/jmeter_perf/report/summary.rb', line 110

def self.read(csv_path)
  summary = new(file_path: csv_path)
  CSV.foreach(csv_path, headers: true) do |row|
    metric = row["Metric"]
    value = row["Value"]

    if metric == "Name"
      summary.name = value
    elsif metric.start_with?("Response Code")
      code = metric.split.last
      summary.response_codes[code] = value.to_i
    elsif metric == "CSV Errors"
      summary.csv_error_lines = value.split(":").map(&:to_i)
    elsif CSV_HEADER_MAPPINGS.key?(metric)
      summary.public_send(:"#{CSV_HEADER_MAPPINGS[metric]}=", value.include?(".") ? value.to_f : value.to_i)
    end
  end
  summary
end

Instance Method Details

#finish!void

This method returns an undefined value.

Marks the summary as finished and joins the processing thread.



160
161
162
163
# File 'lib/jmeter_perf/report/summary.rb', line 160

def finish!
  @finished = true
  @processing_jtl_thread&.join
end

#stream_jtl_asyncvoid

Note:

Once streaming, in order to finish processing, call ‘finish!` otherwise it will continue indefinitely.

This method returns an undefined value.

Starts streaming and processing JTL file content asynchronously.



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/jmeter_perf/report/summary.rb', line 192

def stream_jtl_async
  @processing_jtl_thread = Thread.new do
    Timeout.timeout(@jtl_read_timeout, nil, "Timed out attempting to open JTL File #{@file_path}") do
      sleep 0.1 until File.exist?(@file_path) # Wait for the file to be created
    end

    File.open(@file_path, "r") do |file|
      until @finished && file.eof?
        line = file.gets

        # Skip if the line is nil. This can happen if not @finished, and we are at EOF
        next if line.nil?
        # Process only if the line is complete. JMeter always finishes with a newline
        read_until_complete_line(file, line)
      end
    end
  end

  @processing_jtl_thread.abort_on_exception = true
  nil
end

#summarize_data!void

This method returns an undefined value.

Summarizes the collected data by calculating statistical metrics and error rates.



217
218
219
220
221
222
223
224
# File 'lib/jmeter_perf/report/summary.rb', line 217

def summarize_data!
  @p10, @p50, @p95 = @running_statistics_helper.get_percentiles(0.1, 0.5, 0.95)
  @error_percentage = (@total_errors.to_f / @total_requests) * 100
  @avg = @running_statistics_helper.avg
  @total_run_time = ((@end_time - @start_time) / 1000).to_f  # Convert milliseconds to seconds
  @requests_per_minute = @total_run_time.zero? ? 0 : (@total_requests / @total_run_time) * 60.0
  @standard_deviation = @running_statistics_helper.std
end

#write_csv(output_file) ⇒ void

This method returns an undefined value.

Generates a CSV report with the given output file.

The CSV report includes the following:

  • A header row with “Metric” and “Value”.

  • Rows for each metric and its corresponding value from ‘CSV_HEADER_MAPPINGS`.

  • Rows for each response code and its count from ‘@response_codes`.

  • A row for CSV errors, concatenated with “:”.

Parameters:

  • output_file (String)

    The path to the output CSV file.



175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/jmeter_perf/report/summary.rb', line 175

def write_csv(output_file)
  CSV.open(output_file, "wb") do |csv|
    csv << ["Metric", "Value"]
    CSV_HEADER_MAPPINGS.each do |metric, value|
      csv << [metric, public_send(value)]
    end
    @response_codes.each do |code, count|
      csv << ["Response Code #{code}", count]
    end

    csv << ["CSV Errors", @csv_error_lines.join(":")]
  end
end