Skip to content

Instantly share code, notes, and snippets.

@camertron
Last active August 15, 2022 20:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save camertron/6f003aaf066d0d92629c32df766623f9 to your computer and use it in GitHub Desktop.
Save camertron/6f003aaf066d0d92629c32df766623f9 to your computer and use it in GitHub Desktop.
Speeding up ActionView::OutputBuffer (Vitesse)
# 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
# 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
#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