Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dasherize, Turbolinks 3 and Parallel #5

Open
winston opened this issue Oct 8, 2015 · 0 comments
Open

Dasherize, Turbolinks 3 and Parallel #5

winston opened this issue Oct 8, 2015 · 0 comments
Labels

Comments

@winston
Copy link
Member

winston commented Oct 8, 2015

We open-sourced Dasherize a few days ago.

screenshot 2015-10-08 18 48 13

Dasherize is a simple, material-designed dashboard for your projects on which you can see:

  • CI status of master branch (supports Travis CI, Codeship and CircleCI)
  • GitHub Pull Requests and Issues count and a peek of most recents

More importantly, Dasherize also has a presentation mode for big screen displays.

The README has more details of how Dasherize came about, so you can read that.

This blog post dives more into the technical details.

Turbolinks 3

Dasherize 3 uses Turbolinks 3 🎩. In fact, it's tracking master of Turbolinks now.

Specifically, it uses the Partial Replacement ✨ technique that's only available in Turbolinks 3.

Which Feature?

Turbolinks is used to load each "Card" on the dashboard.

1

Code Walk Through

When the dashboard loads, it first fills the dashboard with empty "Cards" (name only) for each project.

The code can be found in app/views/projects/index.html.slim, and the loop is:

- if @projects.present?
  .row.mar-lg-top
    - @projects.each do |project|
      .col.s12.m4
        .project id="project:#{project.id}"
          = link_to project_path(project), project_path(project), remote: true, class: 'hide js-project'

          .card-panel.no-padding.grey.darken-1
            .card-heading
              .card-title
                = link_to project.repo_name
                .right
                  = link_to icon("gear"), edit_project_path(project), class: "gear"
            .card-status.center.progress
              .indeterminate

The important bit are the two lines below, while the rest are just markup that creates an empty "Card" with a progress bar.

.project id="project:#{project.id}"
  = link_to project_path(project), project_path(project), remote: true, class: 'hide js-project'

The id is important because this is the id to be used for Turbolinks Partial Replacement, so that a specific .project can be swapped out with a server response.

Next, the anchor tag links to the project_path(project) which is a RESTful path to projects#show that shows (the "Card" for) one project.

The magic happens with remote: true and some JavaScript. When the page loads, JavaScript will trigger a click on all the anchor tags with .js-project class.

// app/assets/javascripts/projects.js

$(".js-project").not('.in-progress').addClass('in-progress').click();

As each link has remote: true, each click results in an async call to projects#show which looks like:

# app/controllers/projects_controller.rb

def show
  @project = ProjectDecorator.new(@project)
  @project.process_with(current_user.oauth_account.oauth_token)

  render change: "project:#{@project.id}"
end

If you noticed, the last line of the method show reads render change: "project:#{@project.id}".

Let's break it down:

render with change instructs Turbolinks 3 to render the response (instead of doing a normal page load).

change: "project:#{@project.id}" instructs Turbolinks 3 to replace only the div with a matching id that can be found in the rendering app/views/projects/show.html.slim.

And so, one by one, the empty "Cards" will be replaced by "Cards" with information.

As of this writing, Turbolinks 3's Partial Replacement technique looks really promising to me. In fact, before Turbolinks 3, I would write custom JS that sort of mimics the behavior of Partial Replacement. Hence I am really looking forward to the release of Turbolinks 5 as that means I don't need to write extra JS anymore.

There is a potential problem which I am keeping track of though:

turbolinks/turbolinks-classic#546

Parallel Tests

You are probably familiar with Parallel Tests but not so much of the gem that powers it: Parallel.

If you look into the source code, you will notice that I am actually not storing anything in the database (except for projects). Hence in order to make API calls ((GitHub + CI) * Number of Projects) speedy, Parallel is used to parallelize the API calls.

Back to app/controllers/projects_controller.rb again, where we first instantiate a ProjectDecorator, then we invoke process_with with the user's GitHub OAuth token:

# app/controllers/projects_controller.rb

def show
  @project = ProjectDecorator.new(@project)
  @project.process_with(current_user.oauth_account.oauth_token)

  render change: "project:#{@project.id}"
end

The implementation of process_with is as follows:

# app/models/project_decorator

def process_with(oauth_token=nil)
  @oauth_token = oauth_token

  call_apis
end

The magic in this case happens in the private method call_apis which invokes other methods:

def call_apis
  Parallel.each(api_functions, in_threads: api_functions.size) { |func| func.call }
end

def api_functions
  [
    method(:init_repos),
    method(:init_ci)
  ]
end

def init_repos
  client   = Octokit::Client.new(access_token: @oauth_token)
  @_issues = client.issues(repo_name)
end

def init_ci
  @_ci =
    case ci_type
      when "travis"
        Status::Travis.new(repo_name, travis_token).run!
      when "codeship"
        Status::Codeship.new(repo_name, codeship_uuid).run!
      when "circleci"
        Status::Circleci.new(repo_name, circleci_token).run!
      else
        Status::Null.new
    end
end    

In the method call_apis, Parallel was used to fork 2 threads (api_functions.size), and to split and execute the methods in api_functions in separate threads.

def call_apis
  Parallel.each(api_functions, in_threads: api_functions.size) { |func| func.call }
end

Using method(:init_repos) and method(:init_ci), these two methods become function pointers that we can pass it as arguments to Parallel.each and be eventually invoked with func.call.

As such, to call both GitHub and CI apis for a project, no waiting is required to make the two api calls. With Parallel, it helped to reduce the time required for making all API calls, and thus made the dashboard load speedily.

I had fun building Dasherize as a toy utility project.

I hope you enjoyed reading about some of the technical details too. 😊 Thanks for reading!

@winston ✏️ Jolly Good Code

About Jolly Good Code

Jolly Good Code

We specialise in Agile practices and Ruby, and we love contributing to open source.
Speak to us about your next big idea, or check out our projects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants