Pearware Blog : http://blog.pearware.org/articles.rss en-us 40 agile web development Improving Java web site performance with asset caching <p>In this post, I&#8217;ll be talking about a solution I developed at my day job to improve the performance of our web site by allowing the browser to cache JavaScript, CSS, and image files. We were noticing that much of our traffic was from requests for these assets, rather than our page content. Since these asset files rarely change (once per production deployment), we wanted to have the user&#8217;s browser cache them until the next build.</p> <p>We use <a href="http://developer.yahoo.com/yslow/">Yahoo&#8217;s YSlow</a> Firefox plugin to analyze the performance characteristics of our site and we were getting bad grades for the following category: Add an Expires or a Cache-Control Header. Images are easy enough to cache. Just add an HTTP response header for the images to have them expire 10 years in the future. If you ever need to change an image, instead of modifying it, just create a new one with a different URL. To set this exipres header, I created the following class, called StaticFileFilter:</p> <div class="typocode"><pre><code class="typocode_default ">package org.pearware.web.filter; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author Matt Parrish */ public class StaticFileFilter implements Filter { public void destroy() { } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; long expires = 365*24*60*60*1000; httpResponse.setDateHeader(&quot;Expires&quot;, System.currentTimeMillis() + expires); httpResponse.setHeader(&quot;Cache-Control&quot;, &quot;max-age=&quot; + expires); } public void init(FilterConfig config) throws ServletException { } }</code></pre></div> <p>In web.xml, we add this filter for all images:</p> <div class="typocode"><pre><code class="typocode_default "> &lt;filter&gt; &lt;filter-name&gt;StaticFileFilter&lt;/filter-name&gt; &lt;filter-class&gt;org.pearware.web.filter.StaticFileFilter&lt;/filter-class&gt; &lt;/filter&gt; &lt;filter-mapping&gt; &lt;filter-name&gt;StaticFileFilter&lt;/filter-name&gt; &lt;url-pattern&gt;*.png&lt;/url-pattern&gt; &lt;/filter-mapping&gt; &lt;filter-mapping&gt; &lt;filter-name&gt;StaticFileFilter&lt;/filter-name&gt; &lt;url-pattern&gt;*.jpg&lt;/url-pattern&gt; &lt;/filter-mapping&gt; &lt;filter-mapping&gt; &lt;filter-name&gt;StaticFileFilter&lt;/filter-name&gt; &lt;url-pattern&gt;*.gif&lt;/url-pattern&gt; &lt;/filter-mapping&gt;</code></pre></div> <p>So, now we have our image files cached for 10 years. JavaScript and CSS files, however, tend to change frequently, possibly with every new build. Unlike with images, it really becomes a pain to rename the files, especially when using Source Control and you want to track the changes of the files over time. My solution was to have the build number become part of the URL for these assets, without having to change the names or locations of the files. The StaticFileFilter then becomes responsible for translating the build-dependent URL into the location for the actual resource. Here&#8217;s the new body of the StaticFileFilter.</p> <div class="typocode"><pre><code class="typocode_default "> String buildNumber = SomeWayTo.getBuildNumber(); String oldRequestURI = httpRequest.getRequestURI(); String requestURI = oldRequestURI.replaceFirst(buildNumber + &quot;/(js|css|images)&quot;, &quot;$1&quot;); long expires = 365*24*60*60*1000; httpResponse.setDateHeader(&quot;Expires&quot;, System.currentTimeMillis() + expires); httpResponse.setHeader(&quot;Cache-Control&quot;, &quot;max-age=&quot; + expires); request.getRequestDispatcher(requestURI).forward(request, response);</code></pre></div> <p>What this does is change the requested URI from, say, <strong>/2.3/images/sample.jpg</strong> to <strong>/images/sample.jpg</strong>. The last line of code is responsible for requesting the image at the actual path. The only magic here is how to get the build number. For us, we update a properties file with the build number before every QA build and have a class that reads in the property.</p> <p>The other piece to this is that our XHTML page needs to reference images as <strong>&lt;img src=&#8221;/2.3/images/sample.jpg&#8221;/&gt;</strong> instead of <strong>&lt;img src=&#8221;/images/sample.jpg&#8221;/&gt;</strong>. Since the build number changes with each deployment, we need to dynamically generate this build number. Here&#8217;s how I&#8217;m doing it, using <a href="http://www.freemarker.org">Freemarker</a>:</p> <div class="typocode"><pre><code class="typocode_default ">[#macro css href] &lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;${base}/${someWayToGet.buildNumber}/${href}.css&quot;/&gt; [/#macro] [#macro script src] &lt;script type=&quot;text/javascript&quot; src=&quot;${base}/${someWayToGet.buildNumber}/${src}.js&quot;&gt;&lt;/script&gt; [/#macro] [#macro img src params...] [#compress] &lt;img src=&quot;${base}/${someWayToGet.buildNumber}/${src}&quot;[#list params?keys as attr] ${attr}=&quot;${params[attr]}&quot;[/#list]/&gt; [/#compress] [/#macro] [@script src=&quot;js/prototype&quot;/] [@css href=&quot;css/main&quot;/] [@img src=&quot;images/logo.png&quot; alt=&quot;Logo&quot;/]</code></pre></div> <p>What you can see is that the StaticFileFilter is setup to serve all of our CSS, JavaScript and images. The benefit is that we are now free to modify any of our assets, yet still instruct the browser to cache them for 10 years. When we push out a new build, the browser will see new URL&#8217;s for the assets due to the new build number. Our deployment doesn&#8217;t change; we don&#8217;t need to move files around to support this. It&#8217;s been working great for us. Our YSlow score has gone up and our HTTP traffic for these files has decreased dramatically, freeing up our server and network for serving up the actual content of our application</p> Thu, 01 May 2008 18:04:00 -0500 urn:uuid:0effa26d-c6ea-4314-b180-f429a6cebe9e http://blog.pearware.org/articles/2008/05/01/improving-java-web-site-performance-with-asset-caching#comments web java performance caching http://blog.pearware.org/articles/2008/05/01/improving-java-web-site-performance-with-asset-caching Moved to Denver <p>At the beginning of March, I moved from Columbus, OH to Denver, CO. Nikki and I have a lot of friends here and, so far, we&#8217;ve had a blast hanging out, playing hockey, and hiking.</p> Fri, 18 Apr 2008 12:01:00 -0500 urn:uuid:b1ae7292-977c-4ae0-8ac6-73f52f786e70 http://blog.pearware.org/articles/2008/04/18/moved-to-denver#comments move denver http://blog.pearware.org/articles/2008/04/18/moved-to-denver Reworking Net::SFTP to handle large file downloads <p>I&#8217;m writing an application that downloads access logs from our production servers and runs the <a href="http://awstats.sourceforge.net">AWStats</a> package against them to create the statistics web pages. This process is setup as a Rake task that uses the Net::SFTP library used by <a href="http://www.capify.org">Capistrano</a>, written by Jamis Buck. There is also a front-end Rails application to manage each of the applications to be retrieved. Everything was working great until I tried to grab a 550MB file from one of our servers. Net::SFTP chocked as it ran out of memory.</p> <p>It turns out that the command:</p> <pre><code> sftp.get_file log_file, local_file</code></pre> <p>ends up putting the whole file into memory, which is fine for small files, but not the large one that I was trying to download. Luckily it wasn&#8217;t too bad to refactor my class. Here&#8217;s the new code to achieve the same effect as the above <em>sftp.get_file</em> command.</p> <div class="typocode"><pre><code class="typocode_default "> stat = sftp.stat( log_file ) offset = 0 file_length = stat.size length = 64 * 1024 * 1024 File.open(local_file, File::CREAT|File::TRUNC|File::RDWR, 0644) do |f| while (offset &amp;lt; file_length) sftp.open_handle(log_file) do |handle| data = sftp.read(handle, :length =&amp;gt; length, :offset =&amp;gt; offset) f.write(data) offset += data.length end end end</code></pre></div> <p>This downloads the file in 64MB increments, using only that much memory at any time.</p> Tue, 26 Jun 2007 14:13:00 -0500 urn:uuid:ec2657e4-c48c-4060-aa19-59894f59df7c http://blog.pearware.org/articles/2007/06/26/reworking-net-sftp-to-handle-large-file-downloads#comments ruby rails ruby rails capistrano net::sftp rake http://blog.pearware.org/articles/2007/06/26/reworking-net-sftp-to-handle-large-file-downloads RadiantOnRails released on RubyForge <p>RadiantOnRails is a <a href="http://www.radiantcms.org">Radiant</a> extension I created to allow a Rails application to co-exist with Radiant, giving the developer the best of both (dynamic and static) worlds. You can now visit the new <a href="http://www.rubyforge.org/projects/radiantonrails">project page</a> on RubyForge.</p> <p>This extension will be a major piece for the website I&#8217;m currently working on, <a href="http://www.realidaho.com">RealIdaho.com</a>. Most of the pages for the site just display static content about cities, but there are other portions of the site that will be fully-dynamic Rails pages. This extension allows me to combine both portions of the site into one application so we can develop the Rails pages for displaying the data-driven pages and still leverage the wonderful user interface created by the Radiant team. This will allow the realtor to make content changes without me having to make code changes and redeploy.</p> <p>RadiantOnRails currently allows Radiant snippets to be inserted into Rails views and the next step is to allow the Rails views to use the Radiant layouts so that the look &#8216;n&#8217; feel of the site is consistent, while keeping the views DRY. I&#8217;ll also be working with Loren Johnson to make Radiant available as a plugin which will make integrating Radiant with Rails even easier.</p> Thu, 31 May 2007 14:11:00 -0500 urn:uuid:e29bef12-3824-45ba-8294-f472d8bd9acc http://blog.pearware.org/articles/2007/05/31/radiantonrails-released-on-rubyforge#comments web ruby rails radiant radiantonrails rails extension realidaho http://blog.pearware.org/articles/2007/05/31/radiantonrails-released-on-rubyforge Switching from Pound to Nginx <p>I just switched some <a href="http://www.rubyonrails.com">Ruby on Rails</a> apps I&#8217;m running from <a href="http://www.apsis.ch/pound/">Pound</a> to <a href="http://sysoev.ru/nginx/">Nginx</a> based on the results from some <a href="http://blog.kovyrin.net/2006/08/28/ruby-performance-results/">articles</a> I&#8217;ve read online. The two biggest advantages of Nginx are 1. It&#8217;s raw performance, and 2. It can serve up static files, which is great for running <a href="http://wiki.rubyonrails.org/rails/pages/Capistrano">Capistrano&#8217;s</a> <strong>disable_web</strong> command to show a maintenance page when redeploying an application.</p> <p>At work, we&#8217;re working on a standard Ruby on Rails setup and are currently investigating two options. The first is a <a href="http://mongrel.rubyforge.org/">Mongrel cluster</a> running behind Nginx, as I described above. The second option is fronting the Mongrel cluster with <a href="http://www.lighttpd.net/">Lighttpd</a>. Since the 1.4.x series of Lighttpd is <a href="http://weblog.rubyonrails.org/2006/7/3/pound-makes-lighty-and-mongrel-play-nice">known</a> to have some issues with it&#8217;s mod_proxy implementation, we would use <a href="">Pen</a> until a stable 1.5 version is released.</p> <p>I&#8217;ll post another article once we have finished our evaluation and chosen which option we&#8217;re going to deploy at work. Stay tuned&#8230;</p> Tue, 05 Dec 2006 14:08:00 -0600 urn:uuid:5bc57c3a-1ceb-4323-aa46-63489c4abfe1 http://blog.pearware.org/articles/2006/12/05/switching-from-pound-to-nginx#comments web ruby rails nginx capistrano rails ruby proxy load balancer http://blog.pearware.org/articles/2006/12/05/switching-from-pound-to-nginx Switched to pure ruby ldap library <p>I <a href="http://blog.pearware.org/2005/5/23/ldap-authentication-in-rails">wrote an article awhile back</a> about using the Ruby/LDAP library to handle <span class="caps">LDAP</span> authentication in Ruby on Rails. I just finished swapping out the <span class="caps">LDAP</span> client library in that application from <a href="http://ruby-ldap.sourceforge.net/">Ruby/LDAP</a> to <a href="http://rubyforge.org/projects/net-ldap/">ruby-net-ldap</a>. The problems with Ruby/LDAP are that it isn&#8217;t a <span class="caps">GEM</span>, so installation is a bit more difficult, and it relies on a common <span class="caps">LDAP</span> library, like OpenLDAP, to already be installed on the system. The ruby-net-ldap library is written in pure Ruby, so no other library needs to be installed on the system.</p> <p>Here is the new code that performs the authentication:</p> <div class="typocode"><pre><code class="typocode_default ">require &quot;net/ldap&quot; class User &lt; ActiveRecord::Base def self.authenticate(login, password, host, port) if login.to_s.length &gt; 0 and password.to_s.length &gt; 0 ldap = Net::LDAP.new ldap.host = host ldap.port = port ldap.auth = &quot;cn=#{login},cn=users,o=xyz...&quot;, password if ldap.bin return find(:first, :conditions =&gt; ['username=?', login]) else return false end end end end</code></pre></div> Mon, 20 Nov 2006 13:58:00 -0600 urn:uuid:2a0d0e66-c586-41cc-bde8-f30b0a4dc58a http://blog.pearware.org/articles/2006/11/20/switched-to-pure-ruby-ldap-library#comments ruby rails ruby net ldap rails authentication http://blog.pearware.org/articles/2006/11/20/switched-to-pure-ruby-ldap-library ActiveMailer email performance on site5 <p>The site I am working on, <a href="http://www.realidaho.com">RealIdaho.com</a>, is hosted by <a href="http://www.site5.com">site5</a>. There are some forms that, when filled out by the user, send confirmation emails to the user, to us internally, and to the realtor&#8217;s cellphone. We started getting some reports that the confirmation page wouldn&#8217;t come up, and users were resubmitting the form many times. I discovered a few things while investigating that may be useful to others.</p> <p>The first thing I noticed, is that the page would timeout after 15 seconds and just return a blank page. Not very friendly, and definitely explains why the users were unsure that the form was submitted properly. The site however continued to process the request, and eventually got all the emails out. I contacted the site5 support team and asked the 1) why is the page timing out after only 15 seconds, and 2) why was sending email so slow.</p> <p>Here&#8217;s the answer I got for item 1:</p> <div style="width: 90%; margin: auto; font-style: italic;"> &#8220;Changing the timeout would affect the global settings for apache. This results in a significant increase in the number of open connections that apache has and degrades server performance. I can ask about this but I don&#8217;t think it will be possible to increase the timeout.&#8221; </div> <p>While I understand their reasoning, 15 seconds seems awfully short. I believe the default for apache is 5 minutes. Hopefully, they&#8217;ll consider increasing that amount at least to 30-60 seconds. While I never want a page that takes more than 5 seconds, I&#8217;m sure we&#8217;ll have some pages that do take longer, especially ones that need to communicate with external systems.</p> <p>Regarding number 2, the support technician suggested using sendmail directly instead of sending email over <span class="caps">SMTP</span>, as that should be much faster. So, I went into <span class="caps">RAILS</span>_ROOT/config/environments/production.rb and added the following line:</p> <p>ActiveMailer::Base.delivery_method = :sendmail</p> <p>Here are the performance numbers:</p> <p>Previous method (SMTP):</p> <p>Sending Mail to user (25.22760)<br/> Sending Mail to internal (31.11207)<br/> Sending Mail notification (20.70688)<br/></p> <p>Current method (Sendmail):</p> <p>Sending Mail to user (0.16841)<br/> Sending Mail to internal (0.28479)<br/> Sending Mail notification (0.24701)<br/></p> <p>Wow, that&#8217;s quite a difference! When I tested the form submission, the confirmation page appeared almost immediately. Now that&#8217;s more like it. Now I just need to see about increasing the timeout.</p> Thu, 09 Nov 2006 13:52:00 -0600 urn:uuid:8ece63ce-6a03-48d3-8e90-5b32d72749a0 http://blog.pearware.org/articles/2006/11/09/activemailer-email-performance-on-site5#comments ruby rails rails ActiveMailer sendmail smtp site5 performance http://blog.pearware.org/articles/2006/11/09/activemailer-email-performance-on-site5 End of an Era... Sold my first new car <p>Yesterday I sold the first new car I ever purchased, a silver 2002 Honda Accord to <a href="http://www.carmax.com">CarMax</a>. It&#8217;s always a little sad to lose something like that, but it isn&#8217;t bothering me. I hadn&#8217;t even driven it once over the past month. I was just sitting in my parent&#8217;s driveway doing its best impression of a bird&#8217;s outhouse. It was so unused, that I even had to jumpstart the car twice yesterday! Once at the house to get it going, and then again after I got gas (Maybe I shouldn&#8217;t have left such little gas in the tank). I even told the appraiser to keep the car running for fear of it not starting back up!</p> <p>It had been at my parent&#8217;s house because nikki and I live downtown (Columbus, Ohio), and we only have one parking spot, which we use for our other car, a 2003 Honda Civic. It&#8217;s not bad only having one parking spot, because we only need one car. And that&#8217;s why I could sell the Accord. Now that I&#8217;m working from home and have a scooter, we just don&#8217;t need a second car.</p> <p>Downsizing was one of the advantages that we saw when deciding on living downtown. For most of my errands, taking the scooter, bicycle, or even walking suits my needs. It&#8217;s nice to not have to deal with the maintenance, gasoline, car insurance, registration, etc. It&#8217;s really amazing how much goes into owning a car. Oh yeah&#8230; it was also nice to get some money for it!</p> Fri, 08 Sep 2006 13:50:00 -0500 urn:uuid:396d6861-3afb-46db-84e3-50aa11ab07e4 http://blog.pearware.org/articles/2006/09/08/end-of-an-era-sold-my-first-new-car#comments environment scooter car http://blog.pearware.org/articles/2006/09/08/end-of-an-era-sold-my-first-new-car Offsetting flight emissions <p>I&#8217;m flying back to Phoenix today, as part of my telecommuting agreement. Due to the fact that I&#8217;ll be flying frequently, I feel that I should do something to balance the impact on the environment of my flights. Flying burns a lot of fuel, and flying from Columbus, OH to Phoenix, AZ is certainly no exception. So, today, I&#8217;m purchasing a <a href="http://www.terrapass.com/flight/index2.html">Flight TerraPass</a> to offset the carbon emissions of my flights.</p> Sun, 20 Aug 2006 13:48:00 -0500 urn:uuid:9c206669-a30b-40fa-8d5e-0e5d5151828c http://blog.pearware.org/articles/2006/08/20/offsetting-flight-emissions#comments environment carbon emissions terrapass telecommuting http://blog.pearware.org/articles/2006/08/20/offsetting-flight-emissions Ahh, the easy life <p>This is what my dog does while I work all day&#8230;</p> <p><a href="http://www.flickr.com/photos/mparrish/215397058/"><img src="http://static.flickr.com/96/215397058_9ac81d3aa6_m.jpg"/></a></p> Mon, 14 Aug 2006 13:46:00 -0500 urn:uuid:dc56f853-eb91-49c4-b412-6ecf839e3245 http://blog.pearware.org/articles/2006/08/14/ahh-the-easy-life#comments penelope http://blog.pearware.org/articles/2006/08/14/ahh-the-easy-life