This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
# encoding: utf-8
|
||||
# frozen_string_literal: true
|
||||
require 'ethon'
|
||||
require 'open-uri'
|
||||
require 'patron'
|
||||
require 'curb'
|
||||
require 'net/http'
|
||||
require 'cgi'
|
||||
require 'benchmark/ips'
|
||||
|
||||
require_relative '../spec/support/server'
|
||||
require_relative '../spec/support/localhost_server'
|
||||
|
||||
LocalhostServer.new(TESTSERVER.new, 3000)
|
||||
LocalhostServer.new(TESTSERVER.new, 3001)
|
||||
LocalhostServer.new(TESTSERVER.new, 3002)
|
||||
|
||||
url = 'http://localhost:3000/'.freeze
|
||||
uri = URI.parse('http://localhost:3000/').freeze
|
||||
ethon = Ethon::Easy.new(url: url)
|
||||
patron = Patron::Session.new
|
||||
patron_url = Patron::Session.new(base_url: url)
|
||||
curb = Curl::Easy.new(url)
|
||||
|
||||
puts '[Creation]'
|
||||
Benchmark.ips do |x|
|
||||
x.report('String.new') { '' }
|
||||
x.report('Easy.new') { Ethon::Easy.new }
|
||||
end
|
||||
|
||||
puts '[Escape]'
|
||||
Benchmark.ips do |x|
|
||||
x.report('CGI.escape') { CGI.escape("まつもと") }
|
||||
x.report('Easy.escape') { ethon.escape("まつもと") }
|
||||
end
|
||||
|
||||
puts '[Requests]'
|
||||
Benchmark.ips do |x|
|
||||
x.report('net/http') { Net::HTTP.get_response(uri) }
|
||||
x.report('open-uri') { open url }
|
||||
|
||||
x.report('patron') do
|
||||
patron.base_url = url
|
||||
patron.get('/')
|
||||
end
|
||||
|
||||
x.report('patron reuse') { patron_url.get('/') }
|
||||
|
||||
x.report('curb') do
|
||||
curb.url = url
|
||||
curb.perform
|
||||
end
|
||||
|
||||
x.report('curb reuse') { curb.perform }
|
||||
|
||||
x.report('Easy.perform') do
|
||||
ethon.url = url
|
||||
ethon.perform
|
||||
end
|
||||
|
||||
x.report('Easy.perform reuse') { ethon.perform }
|
||||
end
|
||||
|
||||
puts "[ 4 delayed Requests ]"
|
||||
Benchmark.ips do |x|
|
||||
x.report('net/http') do
|
||||
3.times do |i|
|
||||
uri = URI.parse("http://localhost:300#{i}/?delay=1")
|
||||
Net::HTTP.get_response(uri)
|
||||
end
|
||||
end
|
||||
|
||||
x.report("open-uri") do
|
||||
3.times do |i|
|
||||
open("http://localhost:300#{i}/?delay=1")
|
||||
end
|
||||
end
|
||||
|
||||
x.report("patron") do
|
||||
sess = Patron::Session.new
|
||||
3.times do |i|
|
||||
sess.base_url = "http://localhost:300#{i}/?delay=1"
|
||||
sess.get("/")
|
||||
end
|
||||
end
|
||||
|
||||
x.report("Easy.perform") do
|
||||
easy = Ethon::Easy.new
|
||||
3.times do |i|
|
||||
easy.url = "http://localhost:300#{i}/?delay=1"
|
||||
easy.perform
|
||||
end
|
||||
end
|
||||
|
||||
x.report("Multi.perform") do
|
||||
multi = Ethon::Multi.new
|
||||
3.times do |i|
|
||||
easy = Ethon::Easy.new
|
||||
easy.url = "http://localhost:300#{i}/?delay=1"
|
||||
multi.add(easy)
|
||||
end
|
||||
multi.perform
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,114 @@
|
||||
# frozen_string_literal: true
|
||||
require 'ethon'
|
||||
require 'ethon/easy'
|
||||
|
||||
require_relative 'perf_spec_helper'
|
||||
require 'rspec/autorun'
|
||||
|
||||
describe "low-level interactions with libcurl" do
|
||||
describe Ethon::Multi do
|
||||
memory_leak_test("init") do
|
||||
Ethon::Multi.new
|
||||
end
|
||||
|
||||
memory_leak_test("handle") do
|
||||
Ethon::Multi.new.handle
|
||||
end
|
||||
end
|
||||
|
||||
describe Ethon::Easy do
|
||||
memory_leak_test("init") do
|
||||
Ethon::Easy.new
|
||||
end
|
||||
|
||||
memory_leak_test("handle") do
|
||||
Ethon::Easy.new.handle
|
||||
end
|
||||
|
||||
memory_leak_test("headers") do
|
||||
Ethon::Easy.new.headers = { "a" => 1, "b" => 2, "c" => 3, "d" => 4}
|
||||
end
|
||||
|
||||
memory_leak_test("escape") do
|
||||
Ethon::Easy.new.escape("the_sky&is_blue")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe Ethon::Easy::Form do
|
||||
memory_leak_test("init") do
|
||||
Ethon::Easy::Form.new(nil, {})
|
||||
end
|
||||
|
||||
memory_leak_test("first") do
|
||||
Ethon::Easy::Form.new(nil, {}).first
|
||||
end
|
||||
|
||||
memory_leak_test("last") do
|
||||
Ethon::Easy::Form.new(nil, {}).last
|
||||
end
|
||||
|
||||
memory_leak_test("materialized with some params") do
|
||||
form = Ethon::Easy::Form.new(nil, { "a" => "1" })
|
||||
form.materialize
|
||||
end
|
||||
|
||||
memory_leak_test("materialized with a file") do
|
||||
File.open(__FILE__, "r") do |file|
|
||||
form = Ethon::Easy::Form.new(nil, { "a" => file })
|
||||
form.materialize
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "higher level operations" do
|
||||
memory_leak_test("a simple request") do
|
||||
Ethon::Easy.new(:url => "http://localhost:3001/",
|
||||
:forbid_reuse => true).perform
|
||||
end
|
||||
|
||||
memory_leak_test("a request with headers") do
|
||||
Ethon::Easy.new(:url => "http://localhost:3001/",
|
||||
:headers => { "Content-Type" => "application/json",
|
||||
"Something" => "1",
|
||||
"Else" => "qwerty",
|
||||
"Long-String" => "aassddffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz"},
|
||||
:forbid_reuse => true).perform
|
||||
end
|
||||
|
||||
memory_leak_test("a request with headers and params") do
|
||||
easy = Ethon::Easy.new(:url => "http://localhost:3001/",
|
||||
:headers => { "Content-Type" => "application/json",
|
||||
"Something" => "1",
|
||||
"Else" => "qwerty",
|
||||
"Long-String" => "aassddffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz"},
|
||||
:forbid_reuse => true)
|
||||
easy.http_request("http://localhost:3001/",
|
||||
:get,
|
||||
:params => { "param1" => "value1",
|
||||
"param2" => "value2",
|
||||
"param3" => "value3",
|
||||
"param4" => "value4"})
|
||||
end
|
||||
|
||||
memory_leak_test("a request with headers, params, and body") do
|
||||
easy = Ethon::Easy.new(:url => "http://localhost:3001/",
|
||||
:headers => { "Content-Type" => "application/json",
|
||||
"Something" => "1",
|
||||
"Else" => "qwerty",
|
||||
"Long-String" => "aassddffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz"},
|
||||
:forbid_reuse => true)
|
||||
easy.http_request("http://localhost:3001/",
|
||||
:get,
|
||||
:params => { "param1" => "value1",
|
||||
"param2" => "value2",
|
||||
"param3" => "value3",
|
||||
"param4" => "value4"},
|
||||
:body => {
|
||||
"body1" => "value1",
|
||||
"body2" => "value2",
|
||||
"body3" => "value3"
|
||||
})
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
#### SETUP
|
||||
require 'bundler'
|
||||
Bundler.setup
|
||||
require 'rspec'
|
||||
|
||||
require 'support/localhost_server'
|
||||
require 'support/server'
|
||||
require_relative 'support/memory_test_helpers'
|
||||
|
||||
require 'logger'
|
||||
|
||||
if ENV['VERBOSE']
|
||||
Ethon.logger = Logger.new($stdout)
|
||||
Ethon.logger.level = Logger::DEBUG
|
||||
end
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.before(:suite) do
|
||||
LocalhostServer.new(TESTSERVER.new, 3001)
|
||||
end
|
||||
config.include(MemoryTestHelpers)
|
||||
config.extend(MemoryTestHelpers::TestMethods)
|
||||
end
|
||||
|
||||
MemoryTestHelpers.setup
|
||||
MemoryTestHelpers.logger = Logger.new($stdout)
|
||||
MemoryTestHelpers.logger.level = Logger::INFO
|
||||
MemoryTestHelpers.logger.formatter = proc do |severity, datetime, progname, msg|
|
||||
"\t\t#{msg}\n"
|
||||
end
|
||||
|
||||
if ENV['VERBOSE']
|
||||
MemoryTestHelpers.logger.level = Logger::DEBUG
|
||||
end
|
||||
|
||||
MemoryTestHelpers.iterations = ENV.fetch("ITERATIONS", 10_000).to_i
|
||||
@@ -0,0 +1,76 @@
|
||||
# frozen_string_literal: true
|
||||
require_relative 'ruby_object_leak_tracker'
|
||||
require_relative 'os_memory_leak_tracker'
|
||||
|
||||
module MemoryTestHelpers
|
||||
class << self
|
||||
attr_accessor :gc_proc, :iterations, :logger
|
||||
|
||||
def setup
|
||||
if RUBY_PLATFORM == "java"
|
||||
# for leak detection
|
||||
JRuby.objectspace = true if defined?(JRuby)
|
||||
# for gc
|
||||
require 'java'
|
||||
java_import 'java.lang.System'
|
||||
self.gc_proc = proc { System.gc }
|
||||
else
|
||||
self.gc_proc = proc { GC.start }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module TestMethods
|
||||
def memory_leak_test(description, &block)
|
||||
context(description) do
|
||||
it "doesn't leak ruby objects" do
|
||||
object_leak_tracker = RubyObjectLeakTracker.new
|
||||
track_memory_usage(object_leak_tracker, &block)
|
||||
object_leak_tracker.total_difference_between_runs.should be <= 10
|
||||
end
|
||||
|
||||
it "doesn't leak OS memory (C interop check)" do
|
||||
os_memory_leak_tracker = OSMemoryLeakTracker.new
|
||||
track_memory_usage(os_memory_leak_tracker, &block)
|
||||
os_memory_leak_tracker.total_difference_between_runs.should be <= 10
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def track_memory_usage(tracker)
|
||||
# Intentionally do all this setup before we do any testing
|
||||
logger = MemoryTestHelpers.logger
|
||||
iterations = MemoryTestHelpers.iterations
|
||||
|
||||
checkpoint_frequency = (iterations / 10.0).to_i
|
||||
gc_frequency = 20
|
||||
|
||||
warmup_iterations = [(iterations / 3.0).to_i, 500].min
|
||||
logger.info "Performing #{warmup_iterations} warmup iterations"
|
||||
warmup_iterations.times do
|
||||
yield
|
||||
MemoryTestHelpers.gc_proc.call
|
||||
end
|
||||
tracker.capture_initial_memory_usage
|
||||
|
||||
logger.info "Performing #{iterations} iterations (checkpoint every #{checkpoint_frequency})"
|
||||
|
||||
iterations.times do |i|
|
||||
yield
|
||||
|
||||
last_iteration = (i == iterations - 1)
|
||||
checkpoint = last_iteration || (i % checkpoint_frequency == 0)
|
||||
|
||||
if checkpoint || (i % gc_frequency == 0)
|
||||
MemoryTestHelpers.gc_proc.call
|
||||
end
|
||||
|
||||
if checkpoint
|
||||
logger.info "Iteration #{i} checkpoint"
|
||||
tracker.capture_memory_usage
|
||||
tracker.dump_status(logger)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
class OSMemoryLeakTracker
|
||||
attr_reader :current_run
|
||||
|
||||
def initialize
|
||||
@previous_run = @current_run = 0
|
||||
end
|
||||
|
||||
def difference_between_runs(basis=@previous_run)
|
||||
@current_run - basis
|
||||
end
|
||||
|
||||
def total_difference_between_runs
|
||||
difference_between_runs(@initial_count_run)
|
||||
end
|
||||
|
||||
def capture_initial_memory_usage
|
||||
capture_memory_usage
|
||||
@initial_count_run = @current_run
|
||||
end
|
||||
|
||||
def capture_memory_usage
|
||||
@previous_run = @current_run
|
||||
@current_run = rss_bytes
|
||||
end
|
||||
|
||||
def dump_status(logger)
|
||||
delta = difference_between_runs
|
||||
logger.add(log_level(delta), sprintf("\tTotal memory usage (kb): %d (%+d)", current_run, delta))
|
||||
end
|
||||
|
||||
private
|
||||
# amount of memory the current process "is using", in RAM
|
||||
# (doesn't include any swap memory that it may be using, just that in actual RAM)
|
||||
# Code loosely based on https://github.com/rdp/os/blob/master/lib/os.rb
|
||||
# returns 0 on windows
|
||||
def rss_bytes
|
||||
if ENV['OS'] == 'Windows_NT'
|
||||
0
|
||||
else
|
||||
`ps -o rss= -p #{Process.pid}`.to_i # in kilobytes
|
||||
end
|
||||
end
|
||||
|
||||
def log_level(delta)
|
||||
delta > 0 ? Logger::WARN : Logger::DEBUG
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
# frozen_string_literal: true
|
||||
class RubyObjectLeakTracker
|
||||
attr_reader :previous_count_hash, :current_count_hash
|
||||
|
||||
def initialize
|
||||
@previous_count_hash = @current_count_hash = {}
|
||||
end
|
||||
|
||||
def difference_between_runs(basis=@previous_count_hash)
|
||||
@difference_between_runs ||= Hash[@current_count_hash.map do |object_class, count|
|
||||
[object_class, count - (basis[object_class] || 0)]
|
||||
end]
|
||||
end
|
||||
|
||||
def total_difference_between_runs
|
||||
difference_between_runs(@initial_count_hash).values.inject(0) { |sum, count| sum + count }
|
||||
end
|
||||
|
||||
def capture_initial_memory_usage
|
||||
capture_memory_usage
|
||||
@initial_count_hash = @current_count_hash
|
||||
end
|
||||
|
||||
def capture_memory_usage
|
||||
@difference_between_runs = nil
|
||||
@previous_count_hash = @current_count_hash
|
||||
|
||||
class_to_count = Hash.new { |hash, key| hash[key] = 0 }
|
||||
ObjectSpace.each_object { |obj| class_to_count[obj.class] += 1 }
|
||||
|
||||
sorted_class_to_count = class_to_count.sort_by { |k, v| -v }
|
||||
@current_count_hash = Hash[sorted_class_to_count]
|
||||
end
|
||||
|
||||
def dump_status(logger)
|
||||
diff = difference_between_runs
|
||||
most_used_objects = current_count_hash.to_a.sort_by(&:last).reverse[0, 20]
|
||||
|
||||
most_used_objects.each do |object_class, count|
|
||||
delta = diff[object_class]
|
||||
logger.add(log_level(delta), sprintf("\t%s: %d (%+d)", object_class, count, delta))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def log_level(delta)
|
||||
delta > 0 ? Logger::WARN : Logger::DEBUG
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user