Search code examples
ruby-on-railspdfwkhtmltopdfruby-on-rails-4.2wicked-pdf

High CPU Usage when generating PDFs in Rails with Wicked_PDF gem


I'm trying to generate a PDF file with Rails, but when I do I notice my system CPU starts to max out. Initially, it will go from ~2.5% then increase to ~65%-$80% for a steady period of time and then finally max out almost prior to displaying them PDF in my iframe on my page. Here are some messages I get when monitoring the memory usage on my system:

Warning or critical alerts (lasts 9 entries)
                          2017-06-09 14:58:07 (0:00:04) - CRITICAL on CPU_SYSTEM (100.0)
                          2017-06-09 14:58:04 (0:00:13) - CRITICAL on CPU_USER (Min:72.8 Mean:83.3 Max:93.7)
                          2017-06-09 14:47:39 (0:00:06) - CRITICAL on CPU_USER (93.0)
                          2017-06-09 14:47:29 (0:00:04) - WARNING on CPU_SYSTEM (74.7)
                          2017-06-09 14:36:48 (0:00:04) - CRITICAL on CPU_SYSTEM (100.0)
                          2017-06-09 14:36:45 (0:00:10) - CRITICAL on CPU_IOWAIT (Min:78.6 Mean:85.7 Max:97.4)
                          2017-06-09 14:18:06 (0:00:04) - CRITICAL on CPU_SYSTEM (94.3)
                          2017-06-09 14:18:06 (0:00:07) - CRITICAL on CPU_USER (91.0)
2017-06-09 15:01:14       2017-06-09 14:17:44 (0:00:04) - WARNING on CPU_SYSTEM (73.8)

The gems I have installed for my PDF generation are wicked_pdf (1.0.6) and wkhtmltopdf-binary-edge (0.12.4.0). And the process with code for each are as follows:

controllers/concerns/pdf_player_reports.rb

def director_report_pdf
  @players = Player.where(id: params["player_ids"]

  respond_to do |format|
  format.html
  format.pdf do
    render pdf: "#{params['pdf_title']}",
      template: 'players/director_summary_report.pdf.erb',
        layout: 'print',
        show_as_html: params.key?('debug'),
        window_status: 'Loading...',
        disable_internal_links: true,
        disable_external_links: true,
        dpi: 75,
        disable_javascript: true,
        :margin => {:top => 7, :bottom  => 7, :left => 6, :right => 0},
        encoding: 'utf8'
  end
end

players/director_summary_report.pdf.erb

<div class="document" style="margin-top: -63px;">
  <% @players.each do |player| %>
     <% reports = player.reports.order(created_at: :desc) %>
     <% if player.is_college_player? %>
       <%= render partial: 'college_director_report.html.erb', player: player %>
     <% else %>
       <%= render partial: 'pro_director_report.html.erb', player: player %>
     <% end %>
     <%= "<div class='page-break'></div>".html_safe %>
  <% end %>
</div>

college_director_report.html.erb

<%= wicked_pdf_stylesheet_link_tag "application", media: "all" %>
<%= wicked_pdf_javascript_include_tag "application" %>
<% provide(:title, "#{player.football_name}") %>
<% self.formats = [:html, :pdf, :css, :coffee, :scss] %>

<style>
    thead { display: table-row-group; page-break-inside: avoid }
    tfoot { display: table-row-group; }
    /*thead:before, thead:after { display: none; }*/
    table { page-break-inside: avoid; }
    tr { page-break-inside: avoid; }
    .page-break {
        display:block; clear:both; page-break-after:always;
    }
    .keep-together { page-break-before: always !important; }
    .table-striped>tbody>tr:nth-child(odd)>td,
    tr.found{
        background-color:#e2e0e0 !important;
    }
</style>

<div class="row">
    <div class="col-xs-6">
        <span>DIRECTOR SUMMARY</span>
    </div>
    <div class="col-xs-6 text-right">
        <%= "#{player.full_name} / #{player.school.short_name}".upcase %>
        <h1><%= "#{player.full_name(true)} (#{player.school.code})".upcase %></h1>
    </div>
</div>

<div class="row">
  <div class="col-xs-12">
    <%= render 'directors_report_player_header', player: player %>
    <%= render 'directors_report_workouts', player: player %>
    <%= render 'directors_report_grades', player: player %>
    <%= render 'legacy_directors_report_contacts', player: player %>
  </div>
</div>

directors_report_player_header.html.erb

<table class="table table-condensed table-bordered">
    <thead>
        <tr>
            <th>Name</th>
            <th>School</th>
            <th>#</th>
            <th>Position</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td><%= player.full_name(true) %></td>
            <td><%= player.school.short_name %></td>
            <td><%= player.jersey %></td>
            <td><%= player.position.abbreviation %></td>
        </tr>
    </tbody>
</table>

UPDATE

I ran an example PDF generator using the following and the CPU% is what ends up maxing out as shown below...

enter image description here

 <table class="table table-condensed">
    <thead>
      <th>Number</th>
    </thead>
    <tbody>
      <% (1..60000).each do |number| %>
        <tr>
          <td><%= number %></td>
        </tr>
      <% end %>
    </tbody>
  </table>

Solution

  • Putting this in a controller seems ill-advised because the minute you deploy this the request will take a significant time to generate and block other incoming requests for other pages.

    You should separate this into two concerns. One job that generates the HTML, which could be this controller, and then a background task to convert that HTML into PDF format.

    In your controller, trigger a job using DelayedJob or similar and then render a page that polls for the job having completed.

    Then in your background job you're dealing with just the task of rendering the HTML to PDF, rather than being within a web request. Something along these lines:

    class RendersReportPdf
      def self.call player_ids
        html = ReportsController.render :director_report_pdf, assigns: { players: Player.where(id: player_ids }
        pdf = WickedPdf.new.pdf_from_string html    
        temp = Tempfile.new("#{Time.now.to_i}.pdf")
        temp.write(pdf)
        temp.close
        temp.path
        # Probably upload this to S3 or similar at this point
        # Notify the user that it's now available somehow
      end
    end
    

    If you do this, then you can rule out that the issue is with running WickedPDF from within your controller action, but also you're making sure your site will stay up if you have long-running requests.