Last active
August 15, 2022 20:46
-
-
Save camertron/6f003aaf066d0d92629c32df766623f9 to your computer and use it in GitHub Desktop.
Speeding up ActionView::OutputBuffer (Vitesse)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
require "benchmark/ips" | |
require "allocation_stats" | |
require "/Users/camertron/workspace/camertron/vitesse/ext/vitesse/vitesse" | |
TIMES = 100 | |
actionview_trace = AllocationStats.trace do | |
output_buffer = ActionView::OutputBuffer.new | |
TIMES.times do | |
output_buffer.append="foo" | |
output_buffer.safe_append="bar" | |
end | |
output_buffer.to_s | |
end | |
actionview_total = actionview_trace.allocations.all.size | |
vitesse_trace = AllocationStats.trace do | |
buffer = OutputBuffer.new | |
TIMES.times do | |
buffer.append("foo") | |
buffer.safe_append("bar") | |
end | |
buffer.to_str | |
end | |
vitesse_total = vitesse_trace.allocations.all.size | |
puts "ActionView allocations: #{actionview_total}" | |
puts "Vitesse allocations: #{vitesse_total}" | |
Benchmark.ips do |x| | |
x.report("action_view") do | |
output_buffer = ActionView::OutputBuffer.new | |
TIMES.times do | |
output_buffer.append="foo" | |
output_buffer.safe_append="bar" | |
end | |
output_buffer.to_s | |
end | |
x.report("vitesse") do | |
buffer = OutputBuffer.new | |
TIMES.times do | |
buffer.append("foo") | |
buffer.safe_append("bar") | |
end | |
buffer.to_str | |
end | |
x.compare! | |
end | |
# ActionView allocations: 244 | |
# Vitesse allocations: 2 | |
# | |
# Warming up -------------------------------------- | |
# action_view 1.566k i/100ms | |
# vitesse 2.872k i/100ms | |
# Calculating ------------------------------------- | |
# action_view 15.939k (± 4.4%) i/s - 79.866k in 5.020663s | |
# vitesse 28.135k (±20.8%) i/s - 129.240k in 5.094532s | |
# Comparison: | |
# vitesse: 28135.4 i/s | |
# action_view: 15939.3 i/s - 1.77x (± 0.00) slower |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
TemplateParams = Struct.new(:source, :identifier, :type, :format) | |
template_paths = Dir.glob("./**/*.html.erb") | |
results = {} | |
totals = { unsafe_appends: 0, safe_appends: 0 } | |
template_paths.each_with_index do |template_path, idx| | |
handler = ActionView::Template.handler_for_extension("erb") | |
template = File.read(template_path) | |
template_params = TemplateParams.new({ | |
source: template, | |
identifier: template_path, | |
type: "text/html", | |
format: "text/html" | |
}) | |
compiled = handler.call(template_params, template) | |
unsafe_appends = compiled.count("@output_buffer.append=") | |
safe_appends = compiled.count("@output_buffer.safe_append=") | |
results[template_path] = { unsafe_appends: unsafe_appends, safe_appends: safe_appends } | |
totals[:unsafe_appends] += unsafe_appends | |
totals[:safe_appends] += safe_appends | |
puts "\rInspected #{idx + 1}/#{template_paths.size} templates. Found #{totals[:unsafe_appends]} unsafe appends and #{totals[:safe_appends]} safe appends." | |
end | |
avg_unsafes = (totals[:unsafe_appends] / template_paths.size.to_f).round | |
avg_safes = (totals[:safe_appends] / template_paths.size.to_f).round | |
puts "Average unsafe appends: #{avg_unsafes}" | |
puts "Average safe appends: #{avg_safes}" | |
unsafe_histogram = Hash.new { |h, k| h[k] = 0 } | |
safe_histogram = Hash.new { |h, k| h[k] = 0 } | |
bucket_size = 500 | |
results.each do |_template_path, stats| | |
unsafe_bucket = stats[:unsafe_appends] / bucket_size | |
safe_bucket = stats[:safe_appends] / bucket_size | |
unsafe_histogram[unsafe_bucket] += 1 | |
safe_histogram[safe_bucket] += 1 | |
end | |
puts "####### Unsafe appends #######" | |
unsafe_histogram.keys.sort.each do |k| | |
puts "#{k * bucket_size}-#{((k + 1) * bucket_size) - 1}: #{unsafe_histogram[k]}" | |
end | |
puts | |
puts "####### Safe appends #######" | |
safe_histogram.keys.sort.each do |k| | |
puts "#{k * bucket_size}-#{((k + 1) * bucket_size) - 1}: #{safe_histogram[k]}" | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "ruby.h" | |
#include "ruby/encoding.h" | |
#include "hescape.h" | |
struct Node { | |
VALUE str; | |
unsigned long len; | |
char* raw_str; | |
unsigned long raw_len; | |
struct Node* next; | |
}; | |
struct vt_data { | |
struct Node* head; | |
struct Node* tail; | |
unsigned long len; | |
}; | |
static ID html_safe_id; | |
void vt_data_free(void* _data) { | |
struct vt_data *data = (struct vt_data*)_data; | |
struct Node* current = data->head; | |
while(current != NULL) { | |
struct Node* next = current->next; | |
// if hescape adds characters it allocates a new string, | |
// which needs to be manually freed | |
if (current->len != current->raw_len) { | |
free(current->raw_str); | |
} | |
free(current); | |
current = next; | |
} | |
} | |
void vt_data_mark(void* _data) { | |
struct vt_data *data = (struct vt_data*)_data; | |
struct Node* current = data->head; | |
while(current != NULL) { | |
rb_gc_mark(current->str); | |
current = current->next; | |
} | |
} | |
size_t vt_data_size(const void* data) { | |
return sizeof(struct vt_data); | |
} | |
static const rb_data_type_t vt_data_type = { | |
.wrap_struct_name = "vt_data", | |
.function = { | |
.dmark = vt_data_mark, | |
.dfree = vt_data_free, | |
.dsize = vt_data_size, | |
}, | |
.flags = RUBY_TYPED_FREE_IMMEDIATELY, | |
}; | |
VALUE vt_data_alloc(VALUE self) { | |
struct vt_data *data; | |
data = malloc(sizeof(struct vt_data)); | |
data->head = NULL; | |
data->tail = NULL; | |
data->len = 0; | |
return TypedData_Wrap_Struct(self, &vt_data_type, data); | |
} | |
VALUE vt_append(VALUE self, VALUE str, bool escape) { | |
if (NIL_P(str)) { | |
return Qnil; | |
} | |
struct vt_data* data; | |
TypedData_Get_Struct(self, struct vt_data, &vt_data_type, data); | |
struct Node* new_node; | |
new_node = malloc(sizeof(struct Node)); | |
new_node->len = RSTRING_LEN(str); | |
u_int8_t* raw_str = (u_int8_t*)StringValuePtr(str); | |
if (escape) { | |
new_node->raw_len = hesc_escape_html(&raw_str, raw_str, new_node->len); | |
} else { | |
new_node->raw_len = new_node->len; | |
} | |
new_node->str = str; | |
new_node->raw_str = (char*)raw_str; | |
new_node->next = NULL; | |
if (data->tail != NULL) { | |
data->tail->next = new_node; | |
} | |
if (data->head == NULL) { | |
data->head = new_node; | |
} | |
data->tail = new_node; | |
data->len += new_node->raw_len; | |
return Qnil; | |
} | |
VALUE vt_safe_append(VALUE self, VALUE str) { | |
return vt_append(self, str, true); | |
} | |
VALUE vt_unsafe_append(VALUE self, VALUE str) { | |
if (rb_funcall(str, html_safe_id, 0) == Qfalse) { | |
return vt_append(self, str, true); | |
} | |
return vt_append(self, str, false); | |
} | |
VALUE vt_to_str(VALUE self) { | |
struct vt_data* data; | |
TypedData_Get_Struct(self, struct vt_data, &vt_data_type, data); | |
struct Node* current = data->head; | |
unsigned long pos = 0; | |
char* result = malloc(data->len + 1); | |
while(current != NULL) { | |
memcpy(result + pos, current->raw_str, current->raw_len); | |
pos += current->raw_len; | |
current = current->next; | |
} | |
result[data->len] = '\0'; | |
return rb_str_new(result, data->len + 1); | |
} | |
void Init_vitesse() { | |
html_safe_id = rb_intern("html_safe?"); | |
VALUE klass = rb_define_class("OutputBuffer", rb_cObject); | |
rb_define_alloc_func(klass, vt_data_alloc); | |
rb_define_method(klass, "safe_append", RUBY_METHOD_FUNC(vt_safe_append), 1); | |
rb_define_method(klass, "append", RUBY_METHOD_FUNC(vt_unsafe_append), 1); | |
rb_define_method(klass, "to_str", RUBY_METHOD_FUNC(vt_to_str), 0); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment