Permalink
Browse files

Less allocated objects on each request

How many? Using `memory_profiler` and a Rails app (codetriage.com), master uses:

```
rack/lib x 7318
```

After this patch, the app uses:

```
rack/lib x 4598
```

Or `(7318 - 4598) / 7318.0 * 100 # => 37.16` % fewer objects __PER REQUEST__.

To do this, I extracted really commonly used strings into top level Rack constants. It makes for a bit of a big diff, but I believe the changes are worth it. 

Running benchmark/ips against the same app, I'm seeing a performance host of `2~4%` across the entire app response. This doesn't just make Rack faster, it will make your app faster.

While we could certainly go overboard and pre-define ALL strings as constants, that would be pretty gnarly to work with. This patch goes after the largest of the low hanging fruit.
  • Loading branch information...
schneems committed Oct 1, 2014
1 parent a71be3c commit dc53a8c26dc55d21240233b3d83d36efdef6e924
@@ -22,6 +22,16 @@ def self.version
def self.release
"1.5"
end
PATH_INFO = 'PATH_INFO'.freeze
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
SCRIPT_NAME = 'SCRIPT_NAME'.freeze
QUERY_STRING = 'QUERY_STRING'.freeze
CACHE_CONTROL = 'Cache-Control'.freeze
CONTENT_LENGTH = 'Content-Length'.freeze
CONTENT_TYPE = 'Content-Type'.freeze

GET = 'GET'.freeze
HEAD = 'HEAD'.freeze

autoload :Builder, "rack/builder"
autoload :BodyProxy, "rack/body_proxy"
@@ -17,17 +17,17 @@ def initialize(app, realm=nil, &authenticator)

def unauthorized(www_authenticate = challenge)
return [ 401,
{ 'Content-Type' => 'text/plain',
'Content-Length' => '0',
{ CONTENT_TYPE => 'text/plain',
CONTENT_LENGTH => '0',
'WWW-Authenticate' => www_authenticate.to_s },
[]
]
end

def bad_request
return [ 400,
{ 'Content-Type' => 'text/plain',
'Content-Length' => '0' },
{ CONTENT_TYPE => 'text/plain',
CONTENT_LENGTH => '0' },
[]
]
end
@@ -7,7 +7,7 @@ module Auth
module Digest
class Request < Auth::AbstractRequest
def method
@env['rack.methodoverride.original_method'] || @env['REQUEST_METHOD']
@env['rack.methodoverride.original_method'] || @env[REQUEST_METHOD]
end

def digest?
@@ -4,9 +4,14 @@ def initialize(body, &block)
@body, @block, @closed = body, block, false
end

def respond_to?(*args)
return false if args.first.to_s =~ /^to_ary$/
super or @body.respond_to?(*args)
def respond_to?(method_name)
case method_name
when :to_ary
return false
when String
return false if /^to_ary$/ =~ method_name
end
super or @body.respond_to?(method_name)
end

def close
@@ -4,7 +4,7 @@ module Rack
# status codes).

class Cascade
NotFound = [404, {"Content-Type" => "text/plain"}, []]
NotFound = [404, {CONTENT_TYPE => "text/plain"}, []]

attr_reader :apps

@@ -56,11 +56,11 @@ def call(env)

if ! chunkable_version?(env['HTTP_VERSION']) ||
STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
headers['Content-Length'] ||
headers[CONTENT_LENGTH] ||
headers['Transfer-Encoding']
[status, headers, body]
else
headers.delete('Content-Length')
headers.delete(CONTENT_LENGTH)
headers['Transfer-Encoding'] = 'chunked'
[status, headers, Body.new(body)]
end
@@ -46,9 +46,9 @@ def log(env, status, header, began_at)
env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
env["REMOTE_USER"] || "-",
now.strftime("%d/%b/%Y:%H:%M:%S %z"),
env["REQUEST_METHOD"],
env["PATH_INFO"],
env["QUERY_STRING"].empty? ? "" : "?"+env["QUERY_STRING"],
env[REQUEST_METHOD],
env[PATH_INFO],
env[QUERY_STRING].empty? ? "" : "?"+env[QUERY_STRING],
env["HTTP_VERSION"],
status.to_s[0..3],
length,
@@ -65,7 +65,7 @@ def log(env, status, header, began_at)
end

def extract_content_length(headers)
value = headers['Content-Length'] or return '-'
value = headers[CONTENT_LENGTH] or return '-'
value.to_s == '0' ? '-' : value
end
end
@@ -20,14 +20,14 @@ def initialize(app)
end

def call(env)
case env['REQUEST_METHOD']
when "GET", "HEAD"
case env[REQUEST_METHOD]
when GET, HEAD
status, headers, body = @app.call(env)
headers = Utils::HeaderHash.new(headers)
if status == 200 && fresh?(env, headers)
status = 304
headers.delete('Content-Type')
headers.delete('Content-Length')
headers.delete(CONTENT_TYPE)
headers.delete(CONTENT_LENGTH)
original_body = body
body = Rack::BodyProxy.new([]) do
original_body.close if original_body.respond_to?(:close)
@@ -16,7 +16,7 @@ def call(env)
headers = HeaderHash.new(headers)

if !STATUS_WITH_NO_ENTITY_BODY.include?(status.to_i) &&
!headers['Content-Length'] &&
!headers[CONTENT_LENGTH] &&
!headers['Transfer-Encoding'] &&
body.respond_to?(:to_ary)

@@ -28,7 +28,7 @@ def call(env)
obody.close if obody.respond_to?(:close)
end

headers['Content-Length'] = length.to_s
headers[CONTENT_LENGTH] = length.to_s
end

[status, headers, body]
@@ -20,7 +20,7 @@ def call(env)
headers = Utils::HeaderHash.new(headers)

unless STATUS_WITH_NO_ENTITY_BODY.include?(status)
headers['Content-Type'] ||= @content_type
headers[CONTENT_TYPE] ||= @content_type
end

[status, headers, body]
@@ -54,20 +54,20 @@ def call(env)
case encoding
when "gzip"
headers['Content-Encoding'] = "gzip"
headers.delete('Content-Length')
headers.delete(CONTENT_LENGTH)
mtime = headers.key?("Last-Modified") ?
Time.httpdate(headers["Last-Modified"]) : Time.now
[status, headers, GzipStream.new(body, mtime)]
when "deflate"
headers['Content-Encoding'] = "deflate"
headers.delete('Content-Length')
headers.delete(CONTENT_LENGTH)
[status, headers, DeflateStream.new(body)]
when "identity"
[status, headers, body]
when nil
message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) }
[406, {"Content-Type" => "text/plain", "Content-Length" => message.length.to_s}, bp]
[406, {CONTENT_TYPE => "text/plain", CONTENT_LENGTH => message.length.to_s}, bp]
end
end

@@ -138,7 +138,7 @@ def should_deflate?(env, status, headers, body)
# Skip compressing empty entity body responses and responses with
# no-transform set.
if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
headers['Cache-Control'].to_s =~ /\bno-transform\b/ ||
headers[CACHE_CONTROL].to_s =~ /\bno-transform\b/ ||
(headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
return false
end
@@ -55,8 +55,8 @@ def call(env)

def _call(env)
@env = env
@script_name = env['SCRIPT_NAME']
@path_info = Utils.unescape(env['PATH_INFO'])
@script_name = env[SCRIPT_NAME]
@path_info = Utils.unescape(env[PATH_INFO])

if forbidden = check_forbidden
forbidden
@@ -72,7 +72,7 @@ def check_forbidden
body = "Forbidden\n"
size = Rack::Utils.bytesize(body)
return [403, {"Content-Type" => "text/plain",
"Content-Length" => size.to_s,
CONTENT_LENGTH => size.to_s,
"X-Cascade" => "pass"}, [body]]
end

@@ -101,7 +101,7 @@ def list_directory
@files << [ url, basename, size, type, mtime ]
end

return [ 200, {'Content-Type'=>'text/html; charset=utf-8'}, self ]
return [ 200, { CONTENT_TYPE =>'text/html; charset=utf-8'}, self ]
end

def stat(node, max = 10)
@@ -130,7 +130,7 @@ def entity_not_found
body = "Entity not found: #{@path_info}\n"
size = Rack::Utils.bytesize(body)
return [404, {"Content-Type" => "text/plain",
"Content-Length" => size.to_s,
CONTENT_LENGTH => size.to_s,
"X-Cascade" => "pass"}, [body]]
end

@@ -11,6 +11,7 @@ module Rack
# used when Etag is absent and a directive when it is present. The first
# defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
class ETag
ETAG_STRING = 'ETag'.freeze
DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze

def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
@@ -28,14 +29,14 @@ def call(env)
body = Rack::BodyProxy.new(new_body) do
original_body.close if original_body.respond_to?(:close)
end
headers['ETag'] = %(W/"#{digest}") if digest
headers[ETAG_STRING] = %(W/"#{digest}") if digest
end

unless headers['Cache-Control']
unless headers[CACHE_CONTROL]
if digest
headers['Cache-Control'] = @cache_control if @cache_control
headers[CACHE_CONTROL] = @cache_control if @cache_control
else
headers['Cache-Control'] = @no_cache_control if @no_cache_control
headers[CACHE_CONTROL] = @no_cache_control if @no_cache_control
end
end

@@ -53,8 +54,8 @@ def etag_body?(body)
end

def skip_caching?(headers)
(headers['Cache-Control'] && headers['Cache-Control'].include?('no-cache')) ||
headers.key?('ETag') || headers.key?('Last-Modified')
(headers[CACHE_CONTROL] && headers[CACHE_CONTROL].include?('no-cache')) ||
headers.key?(ETAG_STRING) || headers.key?('Last-Modified')
end

def digest_body(body)
@@ -34,11 +34,11 @@ def call(env)
F = ::File

def _call(env)
unless ALLOWED_VERBS.include? env["REQUEST_METHOD"]
unless ALLOWED_VERBS.include? env[REQUEST_METHOD]
return fail(405, "Method Not Allowed", {'Allow' => ALLOW_HEADER})
end

path_info = Utils.unescape(env["PATH_INFO"])
path_info = Utils.unescape(env[PATH_INFO])
clean_path_info = Utils.clean_path_info(path_info)

@path = F.join(@root, clean_path_info)
@@ -58,19 +58,19 @@ def _call(env)

def serving(env)
if env["REQUEST_METHOD"] == "OPTIONS"
return [200, {'Allow' => ALLOW_HEADER, 'Content-Length' => '0'}, []]
return [200, {'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0'}, []]
end
last_modified = F.mtime(@path).httpdate
return [304, {}, []] if env['HTTP_IF_MODIFIED_SINCE'] == last_modified

headers = { "Last-Modified" => last_modified }
mime = Mime.mime_type(F.extname(@path), @default_mime)
headers["Content-Type"] = mime if mime
headers[CONTENT_TYPE] = mime if mime

# Set custom headers
@headers.each { |field, content| headers[field] = content } if @headers

response = [ 200, headers, env["REQUEST_METHOD"] == "HEAD" ? [] : self ]
response = [ 200, headers, env[REQUEST_METHOD] == "HEAD" ? [] : self ]

# NOTE:
# We check via File::size? whether this file provides size info
@@ -97,7 +97,7 @@ def serving(env)
size = @range.end - @range.begin + 1
end

response[1]["Content-Length"] = size.to_s
response[1][CONTENT_LENGTH] = size.to_s
response
end

@@ -122,8 +122,8 @@ def fail(status, body, headers = {})
[
status,
{
"Content-Type" => "text/plain",
"Content-Length" => body.size.to_s,
CONTENT_TYPE => "text/plain",
CONTENT_LENGTH => body.size.to_s,
"X-Cascade" => "pass"
}.merge!(headers),
[body]
@@ -51,7 +51,7 @@ def self.default(options = {})
options.delete :Port

Rack::Handler::FastCGI
elsif ENV.include?("REQUEST_METHOD")
elsif ENV.include?(REQUEST_METHOD)
Rack::Handler::CGI
elsif ENV.include?("RACK_HANDLER")
self.get(ENV["RACK_HANDLER"])
@@ -26,7 +26,7 @@ def self.serve(app)
"rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http"
})

env["QUERY_STRING"] ||= ""
env[QUERY_STRING] ||= ""
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
env["REQUEST_PATH"] ||= "/"

@@ -59,7 +59,7 @@ def self.serve(request, app)
"rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http"
})

env["QUERY_STRING"] ||= ""
env[QUERY_STRING] ||= ""
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
env["REQUEST_PATH"] ||= "/"
env.delete "CONTENT_TYPE" if env["CONTENT_TYPE"] == ""
@@ -27,7 +27,7 @@ def self.serve(app)
"rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http"
)

env["QUERY_STRING"] ||= ""
env[QUERY_STRING] ||= ""
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
env["REQUEST_PATH"] ||= "/"
status, headers, body = app.call(env)
@@ -78,7 +78,7 @@ def process(request, response)

"rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http"
})
env["QUERY_STRING"] ||= ""
env[QUERY_STRING] ||= ""

status, headers, body = @app.call(env)

@@ -35,9 +35,9 @@ def process_request(request, input_body, socket)
env = Hash[request]
env.delete "HTTP_CONTENT_TYPE"
env.delete "HTTP_CONTENT_LENGTH"
env["REQUEST_PATH"], env["QUERY_STRING"] = env["REQUEST_URI"].split('?', 2)
env["REQUEST_PATH"], env[QUERY_STRING] = env["REQUEST_URI"].split('?', 2)
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
env["PATH_INFO"] = env["REQUEST_PATH"]
env[PATH_INFO] = env["REQUEST_PATH"]
env["QUERY_STRING"] ||= ""
env["SCRIPT_NAME"] = ""

@@ -79,12 +79,12 @@ def service(req, res)
})

env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
env["QUERY_STRING"] ||= ""
unless env["PATH_INFO"] == ""
env[QUERY_STRING] ||= ""
unless env[PATH_INFO] == ""
path, n = req.request_uri.path, env["SCRIPT_NAME"].length
env["PATH_INFO"] = path[n, path.length-n]
env[PATH_INFO] = path[n, path.length-n]
end
env["REQUEST_PATH"] ||= [env["SCRIPT_NAME"], env["PATH_INFO"]].join
env["REQUEST_PATH"] ||= [env["SCRIPT_NAME"], env[PATH_INFO]].join

status, headers, body = @app.call(env)
begin
Oops, something went wrong.

0 comments on commit dc53a8c

Please sign in to comment.