Template vs Context API

23 views
Skip to first unread message

Joshua Peek

unread,
Mar 2, 2010, 10:16:36 PM3/2/10
to Tilt
Template Focused API
--------------------

I really love the current Tilt api because it quite simple, flexible,
and has a clear caching story. Tilt emphasizes templates as the main
model. So its very easy to grasp.

template = Tilt::ERBTemplate.new("foo.erb")
context = Module.new
template.render(context, :foo => "bar")

*Templates are rendered inside contexts*

A context has no requirements. It could be a controller, a template
instance, or even a model record. Super flexible, no contract. Well,
it just depends on "instance_eval" which all Objects support.

Template instances are responsible for their own cache. If a template
is stale, needs to be reloaded or whatever, just drop the reference.
'cache["foo"] ||= Tilt::ERBTemplate.new("foo.erb")' is a viable
caching strategy.


Flip-Flopped Context Focused API
--------------------------------

This is how we do template compiling caching in Rails. I loathe it.
However, its quite fast ;)

We could break alway from generic contexts (just any object) and
require them to implement some behavior. Contexts become the main
model.

*Contexts render templates*

class Context
include Tilt::CompiledTemplates
end

template = Tilt::ERBTemplate.new("foo.erb")
context = Context.new
context.render(template, :foo => "bar") # internally compiles (if
necessary) and calls cached __render_foo_erb

* Contexts have a stricter definition and can't be any object. So you
need to be sure you are rendering against some "complaint" context.
Tilt needs to provide "Tilt::Template" and "Tilt::Context". (twice as
many as one simple class)

* Templates along with their locals need to be hashed into some sort
of unique key to be used as a method. Context method pollution is
another minor concern.

* Contexts hold a set of cached templates. If difficult and not
completely clear how to expire single templates. This usually leads to
memory leaks if you are using one off templates and throwing them
away. Expiring template requires some sort of Tilt.expire(context,
template) api, you can't just forget about it. Template lifespan is
now a prominent concern people need to be more aware of.


----

I have issues with the design philosophy of the context focused
approached. I mainly complaining about using "CompiledTemplates" as a
public API. It doesn't really matter to me how Tilt templates work at
the end of the day. If we could somehow implement the module caching
approach internally and keep the same simple Tilt API, I'm all for
that.

Ryan Tomayko

unread,
Mar 3, 2010, 12:49:57 AM3/3/10
to til...@googlegroups.com
On Tue, Mar 2, 2010 at 7:16 PM, Joshua Peek <jo...@37signals.com> wrote:
> Template Focused API
> --------------------
>
> I really love the current Tilt api because it quite simple, flexible,
> and has a clear caching story. Tilt emphasizes templates as the main
> model. So its very easy to grasp.
>
> template = Tilt::ERBTemplate.new("foo.erb")
> context = Module.new
> template.render(context, :foo => "bar")
>
> *Templates are rendered inside contexts*
>
> A context has no requirements. It could be a controller, a template
> instance, or even a model record. Super flexible, no contract. Well,
> it just depends on "instance_eval" which all Objects support.
>
> Template instances are responsible for their own cache. If a template
> is stale, needs to be reloaded or whatever, just drop the reference.
> 'cache["foo"] ||= Tilt::ERBTemplate.new("foo.erb")' is a viable
> caching strategy.

Nice write up. I couldn't have put it better myself.

I think reorganizing Tilt to be context focused would be a mistake --
better to just create another project that went at it with that
approach.

I remember having a conversation with Yehuda about this at RubyConf. I
didn't fully understand what he was talking about at the time, but now
that you've laid this out it makes a lot more sense. He suggested
making the compiled templates module an option. There would be some
way a module object could be passed to Template::new (or configured
globally) to hold the pre-compiled template methods. Then, in
Template#render, the provided context object would be extended with
the configured CompiledTemplates module and the pre-compiled template
method invoked.

Some more or less random thoughts on this:

1.) If you want the performance benefits of pre-compiling, you'd need
to do a little extra work in providing the CompiledTemplates module.
That shouldn't be an issue for frameworks like Rails or Sinatra where
pretty much all template managing code is in once place.

2.) By providing a CompiledTemplates module explicitly, you're
acknowledging that your context objects will be extended. I did not
like the idea of extending context objects all willy nilly as the
default behavior but I feel better about it in this scenario where you
have to opt in.

3.) Extending the context object with the CompiledTemplates module on
each call to render may be rather expensive. Maybe we check if the
context object respond_to? the compiled template method, and if so,
assume that the module was already included. This way, Rails can
include its CompiledTemplates module in the base View class; Sinatra
would include the CompiledTemplates module in Sinatra::Base.

4.) The problem of cleaning up / expiring old template methods is
punted to whoever provides the CompiledTemplates module. A simple
strategy may be to simply recreate the module every N requests, or
once the number of methods exceeds some threshold.

With this approach, it seems like we could keep the template focused
interface, but still get the performance benefits of precompiling,
although maybe not as seemlessly as with a context focused approach.

Thoughts?

Thanks,
Ryan

Joshua Peek

unread,
Mar 3, 2010, 2:11:34 AM3/3/10
to til...@googlegroups.com
On Tue, Mar 2, 2010 at 11:49 PM, Ryan Tomayko <rtom...@gmail.com> wrote:
> I remember having a conversation with Yehuda about this at RubyConf. I
> didn't fully understand what he was talking about at the time, but now
> that you've laid this out it makes a lot more sense. He suggested
> making the compiled templates module an option. There would be some
> way a module object could be passed to Template::new (or configured
> globally) to hold the pre-compiled template methods. Then, in
> Template#render, the provided context object would be extended with
> the configured CompiledTemplates module and the pre-compiled template
> method invoked.
>
> Some more or less random thoughts on this:
>
> 1.) If you want the performance benefits of pre-compiling, you'd need
> to do a little extra work in providing the CompiledTemplates module.
> That shouldn't be an issue for frameworks like Rails or Sinatra where
> pretty much all template managing code is in once place.

Ideally, you don't have to think about this.

> 2.) By providing a CompiledTemplates module explicitly, you're
> acknowledging that your context objects will be extended. I did not
> like the idea of extending context objects all willy nilly as the
> default behavior but I feel better about it in this scenario where you
> have to opt in.
>
> 3.) Extending the context object with the CompiledTemplates module on
> each call to render may be rather expensive. Maybe we check if the
> context object respond_to? the compiled template method, and if so,
> assume that the module was already included. This way, Rails can
> include its CompiledTemplates module in the base View class; Sinatra
> would include the CompiledTemplates module in Sinatra::Base.
>
> 4.) The problem of cleaning up / expiring old template methods is
> punted to whoever provides the CompiledTemplates module. A simple
> strategy may be to simply recreate the module every N requests, or
> once the number of methods exceeds some threshold.

Cleanup is the dirtiest part. Memory leaks are a serious concern.

> With this approach, it seems like we could keep the template focused
> interface, but still get the performance benefits of precompiling,
> although maybe not as seemlessly as with a context focused approach.

This discussion could easily go out of control when more people chime
in. Lets make sure we are focusing on real implementations.

http://gist.github.com/320391

Option #2 is how you described.

Option #1 is interesting. Tilt could (even optionally) include
CompiledTemplates into Object making any object a valid context. The
cons are "method pollution" on every object. It seems like a valid
concern, but I'm more interested if there are any performance hits for
shoving this into every object. Finalizers should automatically deal
with unused complied methods. The real win is keeping the Tilt api the
same. Maybe one day, Ruby will be fast enough not to need this
compiled method hack and we can revert to instance eval or whatever.


--
Joshua Peek

Ryan Tomayko

unread,
Mar 3, 2010, 3:14:35 AM3/3/10
to til...@googlegroups.com

I don't disagree. But if the code to perform cleanup can be simple
outside of tilt while it must be complex inside of tilt, I'd prefer to
punt it to the outside and let the calling code handle it. I'm
assuming cleanup would require a lot of logic inside of tilt, though.
There may be a clean and simple way to accomplish it.

>> With this approach, it seems like we could keep the template focused
>> interface, but still get the performance benefits of precompiling,
>> although maybe not as seemlessly as with a context focused approach.
>
> This discussion could easily go out of control when more people chime
> in. Lets make sure we are focusing on real implementations.
>
> http://gist.github.com/320391
>
> Option #2 is how you described.

I was thinking of something slightly different. I'll get a branch
together tonight or tomorrow.

> Option #1 is interesting. Tilt could (even optionally) include
> CompiledTemplates into Object making any object a valid context. The
> cons are "method pollution" on every object. It seems like a valid
> concern, but I'm more interested if there are any performance hits for
> shoving this into every object.

Having to include these pre-compiled template methods in Object would
make me cry. You're trying to send me back to Python, I think ;)
There's got to be a better way. Even if there's no performance issues,
I'd still hate to do that if only because it would be a mess listing
methods on objects in IRB and ruby-debug.

Here's a slight modification to option #1 that extends the context
object if the precompiled method doesn't exist:

http://gist.github.com/320420

Yehuda tells me extending objects breaks the method cache so this
could be more trouble than it's worth.

I still like the idea of making the calling code own the module. That
way it can be responsible for including it in context objects.

> Finalizers should automatically deal with unused complied methods.

Just so I'm clear, the idea with using finalizers would be to
automatically remove the method from the module when the Template
instance falls out of reference? That seems like a good idea
regardless of who owns the module.

> The real win is keeping the Tilt api the
> same. Maybe one day, Ruby will be fast enough not to need this
> compiled method hack and we can revert to instance eval or whatever.

That reminds me. I'm not entirely sure using a proc +
instance_eval/instance_exec won't work here. Using a method is
supposed to be a little faster but I don't think it's a huge
difference. The reason that approach was reverted was because it broke
under MRI 1.9.2 due to the arity changes with lambda or something. I'm
not convinced that approach is fundamentally flawed. Before we get
ahead of ourselves going the module/method route, we should make sure
there's not a cleaner way.

I'm going to create a few branches with different approaches and maybe
run some benchmarks.

Thanks,
Ryan

Ryan Tomayko

unread,
Mar 3, 2010, 3:42:12 AM3/3/10
to til...@googlegroups.com
On Wed, Mar 3, 2010 at 12:14 AM, Ryan Tomayko <rtom...@gmail.com> wrote:
> That reminds me. I'm not entirely sure using a proc +
> instance_eval/instance_exec won't work here. Using a method is
> supposed to be a little faster but I don't think it's a huge
> difference. The reason that approach was reverted was because it broke
> under MRI 1.9.2 due to the arity changes with lambda or something.

My bad. It was yield from within a template that broke for some reason
under 1.9:

1) Error:
test_passing_a_block_for_yield(ERBTemplateTest):
LocalJumpError: no block given (yield)
(__TEMPLATE__):1:in `block in template_proc'
/Users/rtomayko/git/tilt/lib/tilt.rb:235:in `instance_eval'
/Users/rtomayko/git/tilt/lib/tilt.rb:235:in `evaluate'
/Users/rtomayko/git/tilt/lib/tilt.rb:112:in `render'
/Users/rtomayko/git/tilt/test/tilt_erbtemplate_test.rb:34:in
`block in <class:ERBTemplateTest>'

It breaks under MRI >= 1.9.1. I'm pretty sure we can figure out a fix.

Thanks,
Ryan

Magnus Holm

unread,
Mar 3, 2010, 4:01:35 AM3/3/10
to Tilt
Excellent discussion!

I did a little benchmark of this file: http://github.com/judofyr/parkaby/blob/master/bench/simple.erb,
and the method approach turned out to be 8 times faster than
instance_eval with a string.

I'm looking at this in a more practical way: Imagine you're writing
the most awesome web framework, and you're writing most of your
templates with ERB. Would you choose Tilt which supports tons of
template engines or would you just implement ERB support yourself and
make template rendering 8 times faster? Unfortunately, I believe many
would take the "performance-route" just to have some shiny numbers in
their announcement.

And the "Custom template evaluation scopes" part of Tilt is nice, but
in reality the context will almost always be some instance of a
Controllers::Base-class and we therefore have a very sensible place to
define the methods (not Object, please).

That said, it's probably possible to keep support for both approaches
without too much work.

As far as I know, proc + instance_eval/instance_exec won't work
because if we're caching it we can't capture any yields within it, and
currently you can't invoke instance_eval/instance_exec with a block.
I'd love to be proven wrong though :-)


On Mar 3, 9:14 am, Ryan Tomayko <rtoma...@gmail.com> wrote:
> On Tue, Mar 2, 2010 at 11:11 PM, Joshua Peek <j...@37signals.com> wrote:

Ryan Tomayko

unread,
Mar 3, 2010, 4:33:21 AM3/3/10
to til...@googlegroups.com
On Wed, Mar 3, 2010 at 1:01 AM, Magnus Holm <jud...@gmail.com> wrote:
> Excellent discussion!
>
> I did a little benchmark of this file: http://github.com/judofyr/parkaby/blob/master/bench/simple.erb,
> and the method approach turned out to be 8 times faster than
> instance_eval with a string.

Yeah. There's a pretty massive perf gain to be had not needing to
parse the source into its nodes each time. Lots of needless object
creation is avoided too. I think the question now is how much better
methods perform vs. procs.

> I'm looking at this in a more practical way: Imagine you're writing
> the most awesome web framework, and you're writing most of your
> templates with ERB. Would you choose Tilt which supports tons of
> template engines or would you just implement ERB support yourself and
> make template rendering 8 times faster? Unfortunately, I believe many
> would take the "performance-route" just to have some shiny numbers in
> their announcement.

Well, Tilt is just an extraction from Sinatra. We were getting tons of
requests to add support for different template languages and didn't
want to take that on as a core piece of Sinatra so we split it out.
The other big motivator for me was that it's always a pain in the ass
trying to get backtraces w/ correct line numbers and sane scope rules
with every template engine I've dealt with. I wanted to know that as
long as I conform to an interface, the engine will do basically what I
want.

Also, great performance is going to be one more reason to choose tilt.
It's not there yet but I'm confident we'll get it nailed down and then
that's just one more benefit you get simply by following a standard
interface.

> And the "Custom template evaluation scopes" part of Tilt is nice, but
> in reality the context will almost always be some instance of a
> Controllers::Base-class

Another place I use tilt and plan to use it a lot more is generating
static files. For instance, most projects I work on usually end up
with a bunch of markdown files + an ERB template under a `doc/`
directory. It's trivial to throw a few Rake tasks together that
generate the content. In those cases, the scope is typically a simple
Hash or even just Object. So I'd like tilt to stay useful in non-web
framework cases as well. I definitely don't want to assume the
existance of a common controller/view object.

> and we therefore have a very sensible place to define the methods (not Object, please).

I do agree that in cases where performance is critical, you're likely
going to have a common base class for context objects and that we
shouldn't ignore this if we have to go the method route.

> That said, it's probably possible to keep support for both approaches
> without too much work.
>
> As far as I know, proc + instance_eval/instance_exec won't work
> because if we're caching it we can't capture any yields within it, and
> currently you can't invoke instance_eval/instance_exec with a block.
> I'd love to be proven wrong though :-)

I'd love for that to be proven wrong too :) I'm playing with it now
and it looks like it might be impossible under 1.9. More soon.

Thanks,
Ryan

Joshua Peek

unread,
Mar 3, 2010, 9:46:32 AM3/3/10
to Tilt
On Mar 3, 2:14 am, Ryan Tomayko <rtoma...@gmail.com> wrote:
> I don't disagree. But if the code to perform cleanup can be simple
> outside of tilt while it must be complex inside of tilt, I'd prefer to
> punt it to the outside and let the calling code handle it. I'm
> assuming cleanup would require a lot of logic inside of tilt, though.
> There may be a clean and simple way to accomplish it.

"Object.include Tilt::CompiledTemplate" was just a crazy idea that
could work. 'AnyObject.methods.grep(/tilt/) #=> lots' is nasty

> Yehuda tells me extending objects breaks the method cache so this
> could be more trouble than it's worth.

Right, extend on the fly is no good.


I put together a #3 sample that combines my first two.

http://gist.github.com/320391

We keep the instance_eval path around so we can eval on any object. If
you know ahead of time some class is going be reused over and over as
a context object, you can opt-in for more speed by including
CompiledTemplates. The caching contract stays the same. The template
is responsible for cleaning up its compiled method cache when its
GC'd.

The cleanup and finalize approach is very cool (if i do say so
myself). I hope there is no technically implementation issues with
doing that.

Magnus Holm

unread,
Mar 3, 2010, 10:47:04 AM3/3/10
to til...@googlegroups.com
The opt-in approach is exactly what I had in mind. The cleanup/finalize looks cool too!

+1 from my performance-focused point-of-view :-)

// Magnus Holm

Ryan Tomayko

unread,
Mar 3, 2010, 11:18:20 AM3/3/10
to til...@googlegroups.com

I have branches up for both the proc and method approaches:

procs: http://github.com/rtomayko/tilt/compare/precomp-procs
methods: http://github.com/rtomayko/tilt/compare/precomp-methods

The procs approach falls down under 1.9 due to the yield from proc
problem. I wasn't able to figure out a way around it. Otherwise, I
love it and it would make life a whole lot easier. It works great on
1.8.7.

I plan on merging the precomp-methods branch after adding finalizers.
It looks a lot like #3, allowing the compile site to be configured on
a template-by-template basis but also has a one-line hack to enable
template compilation on Object:

Tilt.enable_global_compile_site!

The way I plan on using it from Sinatra would be something like:

module Sinatra
module CompiledTemplates
end

class Base
include CompiledTemplates

def render(type, name, options, locals, &block)
Tilt.new(type, options, CompiledTemplates) { # load template data }
end
end
end

I assume Rails would be very similar, assuming this meets all your needs.

I'm pretty happy with how this turned out, to be honest. It's a lot
more complex than procs but it seems to be the best we can do and
forced me to clean up some other crap that's been laying around
forever.

Review and feedback appreciated. Huge thanks to everyone who chipped in on this.

Ryan

Ryan Tomayko

unread,
Mar 3, 2010, 11:26:06 AM3/3/10
to til...@googlegroups.com

You know, I do like how the #3 example just uses the mixin to
determine if the scope supports compilation. That'll make things a ton
cleaner.

Thanks,
Ryan

Magnus Holm

unread,
Mar 3, 2010, 11:38:12 AM3/3/10
to til...@googlegroups.com
After this patch, the tests breaks on 1.8.7 too:

diff --git a/test/tilt_erbtemplate_test.rb b/test/tilt_erbtemplate_test.rb
index b3e1da2..01f47ee 100644
--- a/test/tilt_erbtemplate_test.rb
+++ b/test/tilt_erbtemplate_test.rb
@@ -39,6 +39,7 @@ class ERBTemplateTest < Test::Unit::TestCase
   test "passing a block for yield" do
     template = Tilt::ERBTemplate.new { 'Hey <%= yield %>!' }
     assert_equal "Hey Joe!", template.render { 'Joe' }
+    assert_equal "Hey Moe!", template.render { 'Moe' }
   end
 
   test "backtrace file and line reporting without locals" do

Result:

  1) Failure:
test_passing_a_block_for_yield(ERBTemplateTest) [./test/tilt_erbtemplate_test.rb:42]:
<"Hey Moe!"> expected but was
<"Hey Joe!">.

// Magnus Holm

Ryan Tomayko

unread,
Mar 3, 2010, 12:02:49 PM3/3/10
to til...@googlegroups.com
On Wed, Mar 3, 2010 at 8:38 AM, Magnus Holm <jud...@gmail.com> wrote:
> After this patch, the tests breaks on 1.8.7 too:
> diff --git a/test/tilt_erbtemplate_test.rb b/test/tilt_erbtemplate_test.rb
> index b3e1da2..01f47ee 100644
> --- a/test/tilt_erbtemplate_test.rb
> +++ b/test/tilt_erbtemplate_test.rb
> @@ -39,6 +39,7 @@ class ERBTemplateTest < Test::Unit::TestCase
>    test "passing a block for yield" do
>      template = Tilt::ERBTemplate.new { 'Hey <%= yield %>!' }
>      assert_equal "Hey Joe!", template.render { 'Joe' }
> +    assert_equal "Hey Moe!", template.render { 'Moe' }
>    end
>
>    test "backtrace file and line reporting without locals" do
> Result:
>   1) Failure:
> test_passing_a_block_for_yield(ERBTemplateTest)
> [./test/tilt_erbtemplate_test.rb:42]:
> <"Hey Moe!"> expected but was
> <"Hey Joe!">.
> // Magnus Holm

Which branch is that off of? The precomp-methods branch handles it
fine. The precomp-procs branch may use the first block passed forever.
Actually, yeah, I'm sure that's it.

Also, I've updated the precomp-methods branch to look a bit more like
josh's example #3. There's a simple Tilt::CompileSite mixin. Anything
that includes it gets compiled methods when used as a scope.

Thanks,
Ryan

Joshua Peek

unread,
Mar 3, 2010, 12:47:31 PM3/3/10
to til...@googlegroups.com
On Wed, Mar 3, 2010 at 10:18 AM, Ryan Tomayko <rtom...@gmail.com> wrote:
> I plan on merging the precomp-methods branch after adding finalizers.
> It looks a lot like #3, allowing the compile site to be configured on
> a template-by-template basis but also has a one-line hack to enable
> template compilation on Object:
>
>    Tilt.enable_global_compile_site!
>
> The way I plan on using it from Sinatra would be something like:
>
>    module Sinatra
>      module CompiledTemplates
>      end
>
>      class Base
>        include CompiledTemplates
>
>        def render(type, name, options, locals, &block)
>          Tilt.new(type, options, CompiledTemplates) { # load template data }
>        end
>      end
>    end
>
> I assume Rails would be very similar, assuming this meets all your needs.

I don't think we need multiple compile sites. One "Tilt::CompileSite"
should work fine. It shouldn't be harmful if some of Rails' compiled
templates get in Sinatra's module. Its no huge deal, just think
compile site should be as internal as possible.

+ "__tilt_#{object_id}_#{locals.keys.hash}"

Probably need a better hashing algorithm for locals.


--
Joshua Peek

Ryan Tomayko

unread,
Mar 3, 2010, 1:35:14 PM3/3/10
to til...@googlegroups.com

We only need uniqueness over key names, not values, so that should be
pretty close. If not, we can crc32 the concatenated keys or something
and that should give us a bit more insurance against collisions.

Somewhat related, I ran into all kinds of weirdness with finalizers
and object_id uniqueness. It appears object_ids can be reused long
before the previous object's finalizers are run, causing all kinds of
strangeness. Or something. I had to add a timestamp to the method name
to avoid collisions.

Ryan

Magnus Holm

unread,
Mar 3, 2010, 6:13:52 PM3/3/10
to til...@googlegroups.com
Yeah, I was talking about the proc-branch.

The method-branch looks great. A few comments:

1) Haml generates code too, so it should definitely be possible to use method generation here too. See engine.rb#def_method and precompiled.rb#precompiled_with_ambles in nex3/haml.

2) Why do we use @_out_buf instead of a local variable? Shouldn't the user be able to specify the buffer name instead? Instead of all the funky instance_variable_get-stuff, we could simply wrap the source in:

begin
  __tilt_prev_buf = #{bufname}
  #{source}
ensure
  #{bufname} = __tilt_prev_buf
end

And you can specify the buffer name with something like Tilt.new(:buffer_name => "$something")? Then it would work with both lvars, ivars and gvars (even though we might only want to enable it for ivars and gvars).

3) Are you sure the finalizer works properly and doesn't capture the instance? I always mess those up...

// Magnus Holm

Ryan Tomayko

unread,
Mar 3, 2010, 11:17:45 PM3/3/10
to til...@googlegroups.com
On Wed, Mar 3, 2010 at 3:13 PM, Magnus Holm <jud...@gmail.com> wrote:
> Yeah, I was talking about the proc-branch.
> The method-branch looks great. A few comments:
> 1) Haml generates code too, so it should definitely be possible to use
> method generation here too. See engine.rb#def_method and
> precompiled.rb#precompiled_with_ambles in nex3/haml.

Nice.

> 2) Why do we use @_out_buf instead of a local variable? Shouldn't the user
> be able to specify the buffer name instead? Instead of all the funky
> instance_variable_get-stuff, we could simply wrap the source in:
> begin
>   __tilt_prev_buf = #{bufname}
>   #{source}
> ensure
>   #{bufname} = __tilt_prev_buf
> end
> And you can specify the buffer name with something like
> Tilt.new(:buffer_name => "$something")? Then it would work with both lvars,
> ivars and gvars (even though we might only want to enable it for ivars and
> gvars).

Yeah. Good catch. This might be a regression Sinatra 1.0.a now that
you mention it.

> 3) Are you sure the finalizer works properly and doesn't capture the
> instance? I always mess those up...

I'll see about adding a test that verifies the finalizers aren't
holding onto to anything. I verified they were running with puts
debugging but it'd be good to having some a bit more robust in there
to catch any issues in the future.

Thanks,
Ryan

Reply all
Reply to author
Forward
0 new messages