Madogiwa Blog

主に技術系の学習メモに使っていきます。

Rails GuidesのConfiguring Rails Applicationsの項目にcontributeしました📕

rails guideのconfig.cache_classesの説明を修正してcontributeしました🙌

github.com

rails 6にversion upする中でRailsDiffを見てたら、テスト環境のconfig.cache_classesのデフォルト値が変わっていましたが、 http://railsdiff.org/5.2.3/6.0.0

guideの説明では、test環境ではdefault値がtrueになるという記載のままでした🤔

そもそもrails 6でconfig.cache_classesにどういう変更が入っていたのかを追ってみました👀

👇変更が入ったコミットがこちら

github.com

rails new時のSpringのinstall有無によってデフォルト値が変更されるようになったようです。

<%- # railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt %>
  <%- if spring_install? -%>
  config.cache_classes = false
  <%- else -%>
  config.cache_classes = true
  <%- end -%>

Rails Guideのupgrade guideを見ると、Springがインストールされていると変更時にClassのロードが走るので、 cache_classesfalseにしロードを有効にする必要があるとのことでした。 https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#spring-and-the-test-environment

cache_classesを`truePにするとClassのロード処理が走らなくなるからSpringのリロードとバッティングして上手く動かなくなるてきな感じなんですかね? 🤔

最初は、とりあえずSpringがデフォルトでrailsにインストールされる以上、test環境のdefault値はfalseになるとしといたほうがいいかなと思い、 rails guideを下記のように修正するのが良いのでは?ということでPRを出したのですが、

  • config.cache_classes controls whether or not application classes and modules should be reloaded on each request. Defaults to false in development and test modes, and true in production mode.

やはりSpringのinstall有無によって分かれる旨を記載しておいたほうが良いよねというのをコミッターの方に指摘して頂いたので、最終的に下記のように修正しました🙇‍

  • config.cache_classes controls whether or not application classes and modules should be reloaded on each request. Defaults to false in development mode, and true in production mode. In test mode, the default is false if Spring is installed, true otherwise.

(英語に関しては、自分の英語力がなさすぎてコミッターの方に指摘頂いた通りに治してます、ありがとうありがとうございました🙇‍♂️💦)

自分で使ってるOSSにcontributeして還元するのはいいことですが今回のようなガイドとか文章系の修正だと英語力が足りなすぎてかえってコストになってしまっているような気もしたので、英語力身につけたいですね😭

Rubyで作るオレオレWebフレームワーク「第3回 クエリ文字列を含んだGETに対応したRouting機能作る🛣」

RubyでオレオレWebフレームワークを作っているのですが、だいぶ形になってきたので知識の整理がてら色々まとめてみる✍
今回は第3回目です🤟

👇第2回 GETに対応したRouting機能を作る🛣はこちら https://madogiwa0124.hatenablog.com/entry/2019/09/01/233842

今回のゴール

前回は、GETリクエストに対応したRouting機能を作りましたが、それをクエリ文字列を受け取れるように修正します👩‍💻 下記のように定義したら/hello?word="hoge"にアクセス時に'Hello hoge.'が表示出来るような機能を作ります。

require_relative './lib/main.rb'

router.get '/hello' do |params|
  "Hello #{params['word']}."
end

太字で記載したようにリクエスト時にクエリ文字列を追加で取得し、routeで定義したProcオブジェクトを実行する際に引数として渡します📦

  • WEBrickからリクエスト(URL、METHOD、クエリ文字列)取得
  • appmainで定義したDSLを使って作成したrouterで管理しているroutingをリクエストのURLとMETHODから探索
  • 見つかったroutingのProcオブジェクトを返却
  • applicationでProcオブジェクトの引数にクエリ文字列を渡して実行
  • 結果からレスポンスを生成
  • WEBrickにレスポンスを返却

まずはリクエスト時にクエリ文字列を取得する。

まずはリクエストからURLに設定されたクエリ文字列を取得していきます。
application.rbでそのまま実装してもいいのですが、処理が結構多くなってきたのでRack::Requestを継承した独自クラスを作ってそちらに実装していきます👩‍💻

👇実装はこんな感じ、envを引数でとってurlmethodqueryを設定する処理とクエリ文字列で渡された値を返却するparamsメソッドを追加しています🙋‍

require 'rack'

class MyRequest < Rack::Request
  def initialize(env)
    super
    @url = env['REQUEST_URI']
    @method = env['REQUEST_METHOD']
    @query = URI.parse(url).query
  end

  def params
    return unless query
    @params ||= Hash[URI.decode_www_form(query)]
  end

  attr_reader :url, :method, :query
end

rubyの標準ライブラリのURIを使うとURI.parse(url).queryでクエリ文字列部分が取得出来て、URI.decode_www_formの結果をHashの引数で渡すと簡単にHash形式で取得出来るので便利ですね✨

docs.ruby-lang.org

👇実際にapplication.rbに組み込んだ形がこちら

require 'my_request.rb'
require 'rack'

class Application

  def initialize(router:)
    @router = router
  end

  attr_reader :router, :request

  def call(env)
    @request = MyRequest.new(env)
    body = router.bind(url: request.url, method: request.method).process.call
    ['200', {'Content-Type' => 'text/html'}, [body]]
  end

  def run!
    Rack::Handler::WEBrick.run self
  end
end

RoutingのProcオブジェクトにクエリ文字列を渡す

リクエストに設定されたクエリ文字列を取得出来るようになったので、今度はそれをroutingで定義したProcオブジェクトに渡していきます🙋‍
これは簡単で単純にcallの引数にrequest.paramsを渡すだけです👍

require 'my_request.rb'
require 'rack'

class Application

  def initialize(router:)
    @request = MyRequest.new(env)
    @router = router
  end

  attr_reader :router, :request

  def call(env)
    body = router.bind(url: request.url, method: request.method).process.call(request.params)
    ['200', {'Content-Type' => 'text/html'}, [body]]
  end

  def run!
    Rack::Handler::WEBrick.run self
  end
end

これで下記のようなroutingを定義してhttp://localhost:8080/hello?word=hogeにアクセスすると、、、

router.get '/hello' do |params|
  "Hello #{params['word']}"
end

f:id:madogiwa0124:20190908213221p:plain

無事にクエリ文字列が反映されました🙌

おわりに

今回はクエリ文字列を含んだGETに対応したRouting機能を作りました👩‍💻
ここまで出来ると文字列のみを返すような静的サイトだったらなんとか実現できるぐらいには出来てきましたね👍

次回はViewのレンダリング処理を実装していこうかなと思います 🙋‍♀️ レンダリングが出来るとちょっとフレームワークっぽくなってきた感じがしますね✨

Rubyで作るオレオレWebフレームワーク「第2回 GETに対応したRouting機能を作る🛣」

RubyでオレオレWebフレームワークを作っているのですが、だいぶ形になってきたので知識の整理がてら色々まとめてみる✍
今回は第二回目です✌️

👇第1回 Hello World 🌏はこちら https://madogiwa0124.hatenablog.com/entry/2019/08/25/181608

今回のゴール

今回は、GETリクエストに対応したRouting機能を作ります👩‍💻
下記のように定義したら/helloにアクセス時に'Hello My Framework.'が表示されるように出来るような機能を作ります。

require_relative './lib/main.rb'

router.get '/hello' do
  'Hello My Framework.'
end

処理のイメージはこんな感じです👀

f:id:madogiwa0124:20190901215854p:plain

  • WEBrickからリクエスト(URL、METHOD)を取得
  • appmainで定義したDSLを使って作成したrouterで管理しているroutingをリクエストのURLとMETHODから探索
  • 見つかったroutingのProcオブジェクトを返却
  • applicationでProcオブジェクトを実行
  • 結果からレスポンスを生成
  • WEBrickにレスポンスを返却

まずはRoutingを行うClassを作る

Routingを管理するClassRouterを作っていきます。

Routerの役割は2つです。 * getメソッドでroutesにroutingを追加する。 * bind!メソッドでroutesをURLとMETHODから検索してRouteのオブジェクトを返す。

👇実装はこんな感じ

# frozen_string_literal: true

require 'uri'

class Router
  attr_accessor :routes

  def initialize
    @routes = []
  end

  def get(path, &block)
    @routes << Route.new(path: path, process: block, method: 'GET')
  end

  def bind!(url:, method:)
    # urlのpathの部分だけ取得 ex. `http://localhost:8080/hello` -> `/hello`
    path = URI.parse(url).path
    routes.find { |route| route.path == path && route.method == method }
  end

  class Route
    def initialize(path:, process:, method:)
      @path = path
      @process = process
      @method = method
    end

    attr_reader :path, :process, :method
  end
end

getメソッドでblock引数をとっているので、下記のような形で呼び出されると、

router = Router.new
router.get '/hello' do
  'Hello My Framework.'
end

👇下記のようなオブジェクトが返却されます。routerはこれの配列をroutesに保持しているので、bind!メソッドを使ってroutesをmethodとurlで検索して該当のロジックを返却する仕組みです🕵️‍

 #<Router::Route:0x00007ff3c68528e0
  @method="GET",
  @path="/hello",
  @process=#<Proc:0x00007ff3c6852930@app.rb:10>>,

DSLをmainに定義してみる

Routerクラスを作りましたが今のままだと良い感じに使えないので、mainにメソッドを追加して良い感じ定義出来るようにします。

# frozen_string_literal: true

require_relative './router.rb'
require_relative './application.rb'

def router
  @router ||= Router.new
end

# `Application`のオブジェクトを生成する際に`router`を渡すようにしています。
at_exit { Application.new(router: @router).run! }

実装はシンプルで、routerメソッドを定義して、その中でRouterのオブジェクトを返しているだけです。

これでapp.rbでrouter.getを使ってroutingを定義出来るようになりました🎉

require_relative './lib/main.rb'

router.get '/hello' do
  'Hello My Framework.'
end

Requestからroutesを探索してResponseを返す

今回からちょっとロジックが増えてきたので、routerを引数で取りたいので、Procを使うのではなく、独自のApplicationクラスを作っていきます。

第一回で説明したとおり下記ルールを守れば何でもRack上に乗せられます 🙌

  • callメソッドが定義されていること
  • 環境(リクエスト)を受け取る引数を一つとること
  • status、header、bodyの配列を返却すること

👇実際に実装したClassが下記です。

require 'rack'

class Application

  def initialize(router:)
    @router = router
  end

  attr_reader :router

  def call(env)
    body = router.bind(url: env['REQUEST_URI'], method: env['REQUEST_METHOD']).process.call
    ['200', {'Content-Type' => 'text/html'}, [body]]
  end

  def run!
    Rack::Handler::WEBrick.run self
  end
end

やっていることは、envWEBrickから受け取ったリクエスト(env)をもとにrouter.bindroutesから該当するrouteを取得して、app.rbで定義したblock内の処理を実行して、結果をレスポンスのbodyに詰めて返却してます 📦✨

これで/helloでアクセスしたときにMy Framework.と表示されるようになりました🎊

f:id:madogiwa0124:20190901232556p:plain

おわりに

今回はGETとURLのみに対応したRouting機能を作ってみました!今回の実装は結構Sinatraのroutingを結構参考にしています 🎩 Sinatraでは今回のrouterのようなDSLを提供するためにApplicationに定義したメソッド群をmainに委譲するDelegaterというmoduleを活用していますが、これは結構黒魔術感がある + まだメソッドの数も少ないので今回はmainにそのまま定義していく方法を採用しています 💦

sinatra/base.rb at 070d6db3ce3d064d113b44062b401a8d01bbe248 · sinatra/sinatra · GitHub

気になる人は、👆のコードを読んでみると良いかと思います👀

次回はroutingの仕組みをGETパラメーターに対応する等ブラッシュアップしていくような内容にしていこうかなと思っています(routing周りの話はあと2回ぐらい続く予定です)🛣

参考

sinatrarb.com

Rubyで作るオレオレWebフレームワーク「第1回 Hello World 🌏」

RubyでオレオレWebフレームワークを作っているのですが、だいぶ形になってきたので知識の整理がてら色々まとめてみる✍

今回のゴール

最終的には、CRUDが動くようなアプリケーションを作れるような感じになるはずですが、 今回は下記のような実装でlocalhost:8080にアクセスした際にHello Worldがブラウザに表示出来るようなところまでを整理します🌏

まず最初の構成は下記のような形です、シンプル🐥

f:id:madogiwa0124:20190825180642p:plain

処理の流れはこんな感じです。

  1. ruby app.rbWEBrickが起動
  2. ブラウザでアクセスした際にrequestをWEBrickへ送信
  3. WEBrickへのrequestをenvとしてRackを経由してapplicationへ渡す
  4. applicationがresponse生成してRackを経由してWEBrickへ渡す
  5. WEBrickがresponseをブラウザに返却

Rackを使ってWEBrickとコードを繋ぐ 🤝

rubyのコードからWEBrickを起動するためにはRackを使います📦

Rackは、様々なWebServerへのシンプルなインターフェースを提供してくれるgemです💎

下記のルールを守ればRackを使って各種WebServerとやりとりすることが出来ます。

Rack applications A Rack application is a Ruby object (not a class) that responds to call. It takes exactly one argument, the environment and returns an Array of exactly three values: The status, the headers, and the body. https://www.rubydoc.info/github/rack/rack/master/file/SPEC

簡単に説明すると下記のような感じです。

  • callメソッドが定義されていること
  • 環境(リクエスト)を受け取る引数を一つとること
  • status、header、bodyの配列を返却すること

なので下記のようなprocオブジェクトもRackのルールを満たしており、WebServerとやりとりすることが出来ます、Rackすごい🙌

Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ["Hello World"]] }

WEBrickとRackを使ってレスポンスを返す🤟

それでは実際にruby app.rbWEBrickを起動し、Rackを使ってレスポンスHello Worldを返却するコードを見ていきましょう👀

require 'rack'

class Application
  def run!
    # Rackのルールに沿ったレスポンスを返却するコード
    app = Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ["Hello World"]] }
    # 上記コードを乗せてWEBrickを起動
    Rack::Handler::WEBrick.run app
  end
end

# ruby app.rbの処理が終了したらApplication.run!を実行する
at_exit { Application.new.run! }

先程説明したRackのルールを守ったProcオブジェクトを作成して、変数appに格納します。

そしてRack::Handler::WEBrick.runでRackを使ってWEBrickを起動します。このとき引数にappを渡すことで、リクエスト時にappに入れたProcオブジェクトが実行され、結果がレスポンスとして返却されます。

at_exitで起動用のメソッドを実行することでruby app.rbが終了したらWEBrickが定義したコードが乗った状態で起動します🤖

実際の実行結果が下記の通りです。

$ ruby app.rb 
[2019-08-25 18:02:05] INFO  WEBrick 1.4.2
[2019-08-25 18:02:05] INFO  ruby 2.6.3 (2019-04-16) [x86_64-darwin18]
[2019-08-25 18:02:05] INFO  WEBrick::HTTPServer#start: pid=40353 port=8080

localhost::8080にアクセスするとHello Worldが表示されます 🙌

f:id:madogiwa0124:20190825180802p:plain

おわりに

今回はRackを使ってWEBrickを起動し、Hello Worldをレスポンスとして返すところまでの実装を整理してみました✍
シンプルなルールに沿っていれば簡単に様々なWebServerとやりとり出来るRackという仕組みは分かりやすいし、とても便利ですね💎

次回はHTTP METHOD/URLと実行するロジックを紐付けるroutingの仕組みを整理していこうかなと思っています🛣

参考

github.com

qiita.com

Ruby: gem `amatch` を使って文字列同士の類似度を測る👩‍🔬

こんにちは、まどぎわです(・∀・)
rubyで文字の類似度とか測るgemとか無いかなーと探してたらamatchというgemを見つけたので、使い方とかをメモしておきます✍

amatchとは?

This is a collection of classes that can be used for Approximate matching, searching, and comparing of Strings. They implement algorithms that compute the Levenshtein edit distance, Sellers edit distance, the Hamming distance, the longest common subsequence length, the longest common substring length, the pair distance metric, the Jaro-Winkler metric. https://github.com/flori/amatch#description

私は全然詳しくないのでよくわからないのですが、色々な文字の類似度を計測するためのアルゴリズム(ハミング距離、レーベンシュタイン距離等)を使うことが出来るgemみたいです👀

github.com

最後のリリースがon 5 Jul 2017なのが気になりますが、starも330とそこそこあるみたいですね⭐

使い方

👇使い方は下記の通り結構簡単です。

require 'amatch'
include Amatch

m = Sellers.new("pattern")
# => #<Amatch::Sellers:0x40366324>
m.match("pattren")
# => 2.0

該当するアルゴリズムのClassのinitializeに対象の文字列を渡してオブジェクトを生成し、 matchメソッドの引数に比較対象の文字列を渡すと結果が返却されます👩‍🔬

実際に解析してみた

今回は下記のような簡単なwrapperクラスを用意してPairDistanceを使って試してみました👀

require 'amatch'

class SimilarMeasurer
  include Amatch

  def self.pair_distance_call(target, comparison)
    PairDistance.new(target).match(comparison)
  end
end

まずは普通の短い文字でやってみるとこんな感じになりました。1に近ければ1近いほど似ている文字のようです👀

a = 'type'
b = 'typo'
puts SimilarMeasurer.pair_distance_call(a, b)
#=> 0.6666666666666666

ヤフーニュースからとってきた全然違う記事のタイトルで、試してみた結果。

a = '常磐道あおり運転で指名手配の男、大阪で逮捕'
b = '世界で最もハンサムな顔ランキング 世界一は日本人デザイナーが見出したヒーロー'
puts SimilarMeasurer.pair_distance_call(a, b)
#=> 0.12571428571428572

ヤフーニュースからとってきた似たような記事のタイトルで、試して結果。やはり似ている記事の方が先程よりも類似度が高くなりましたね🙌

a = '常磐道あおり運転で指名手配の男、大阪で逮捕'
b = '「あおり男」確保、知人とみられる女性も警察車両に'
puts SimilarMeasurer.pair_distance_call(a, b)
#=> 0.2556390977443609

パフォーマンス

1000回、PairDistanceを使って実施してみましたけど、パフォーマンスも結構いいですね👀

a = 'サンプルサンプルサンプルサンプルサンプル'
targets = (1..1000).map { 'サンプルサンプルサンプルサンプルサンプルサンプルサンプル' }
Benchmark.bm(10){ |r| r.report { targets.each { |b| SimilarMeasurer.pair_distance_call(a, b) } } }

    user     system      total        real
0.004143   0.000100   0.004243 (  0.004276)

おわりに

文字列解析とかを検索するとpythonの情報がかなり多くて、こういう分野ではやはり強いんですね🐍
でもrubyでもこういうライブラリがあったりとか自分が知らないだけで沢山あるのかもしれないですね🙌

参考

レーベンシュタイン距離 - Wikipedia

ハミング距離 - Wikipedia

Ruby: SinatraのHTMLが生成され画面に返却されるまでのコードを読んでみた。

Sinatraコールドリーディングも今回で最終回です🎩
今回はerbでテンプレートを指定して実行後に実際にブラウザに返却されるresponse bodyが生成されるまでを見ていきます!

👇前回までの内容はこちら、以前の記事を前提として書いてますので1回目、2回目を読んで頂いた方が分かりやすいかと思います。

madogiwa0124.hatenablog.com

madogiwa0124.hatenablog.com

目次

はじめに

サンプルのアプリケーションの実装は下記の通りです。

app.rb

require 'sinatra'

get '/' do
  @title = "index"
  @content = "Hello World"
  erb :index
end

views/layout.erb

<html>
<head>
  <title><%= @title %></title>
</head>
<body>
  <%= yield %>
</body>
</html>

views/index.erb

<h1>Sinatra Sample</h1>
<p>message:<%= @content %></p>

上記コードから下記画面のHTMLが生成されブラウザに返却されるまでを見ていきます! f:id:madogiwa0124:20190812120454p:plain

前回で、requestが発生した際にgetで定義した内容が実行されるまでは見たのでので、 今回はerbが実行されて実際にresponseのbodyが生成されるまでをみていきます。

まず前提知識としてgetメソッド内でインスタンス変数が定義されていますが、このインスタンス変数は、Sinatra::Appricationインスタンス変数になるということを覚えておきましょう。

require 'sinatra'

get '/' do
  @title = "index"
  @content = "Hello World"
  erb :index
end

実際に読んでいるコードはこのファイルに記載されている内容です 👩‍💻

sinatra/base.rb at master · sinatra/sinatra · GitHub

テンプレートの取得とHTMLテキストの生成

では早速、最初に実行されるerbメソッドから見ていきます。 erbメソッドを実行すると引数に:erbを渡してrenderメソッドが実行されます。

def erb(template, options = {}, locals = {}, &block)
  render(:erb, template, options, locals, &block)
end

renderメソッドの中では、

  • optionの生成
  • compile_templateを使ってレンダリングに使用するtemplateのオブジェクトを生成
  • template.renderを使ってテンプレートエンジンを使ったHTMLテキストの生成
  • layoutのレンダリング処理

ちなみに、このrenderメソッドはindex.erbレンダリングlayout.erbレンダリングの2回実行されます。

def render(engine, data, options = {}, locals = {}, &block)
  # merge app-level options
  engine_options = settings.respond_to?(engine) ? settings.send(engine) : {}
  options.merge!(engine_options) { |key, v1, v2| v1 }

  # extract generic options
  locals          = options.delete(:locals) || locals         || {}
  views           = options.delete(:views)  || settings.views || "./views"
  layout          = options[:layout]
  layout          = false if layout.nil? && options.include?(:layout)
  eat_errors      = layout.nil?
  layout          = engine_options[:layout] if layout.nil? or (layout == true && engine_options[:layout] != false)
  layout          = @default_layout         if layout.nil? or layout == true
  layout_options  = options.delete(:layout_options) || {}
  content_type    = options.delete(:default_content_type)
  content_type    = options.delete(:content_type)   || content_type
  layout_engine   = options.delete(:layout_engine)  || engine
  # ここでscopeに`Sinatra::Application`のインスタンスが格納される。
  scope           = options.delete(:scope)          || self
  options.delete(:layout)

  # set some defaults
  options[:outvar]           ||= '@_out_buf'
  options[:default_encoding] ||= settings.default_encoding

  # ここからテンプレートを取得してレンダリングする。
  # templateにテンプレートを使ったレンダリング用のオブジェクトを入れて、
  # outputに実際のレンダリング済みのHTMLテキストが格納される。
  # compile and render template
  begin
    layout_was      = @default_layout
    @default_layout = false
    template        = compile_template(engine, data, options, views)
    # scopeの中に設定したインスタンス変数(@content, @title)が格納されているので、
    # template.renderの引数に渡すことでviewとインスタンス変数が紐づき、
    #  :scope=>
    #   <Sinatra::Application:0x00007fed46258268
    #     @content="Hello World",
    #     @title="index"
    # 最終的なHTMLテキストが返却される。
    output          = template.render(scope, locals, &block)
  ensure
    @default_layout = layout_was
  end

  # render layout
  if layout
    options = options.merge(:views => views, :layout => false, :eat_errors => eat_errors, :scope => scope).
            merge!(layout_options)
    # ここでlayoutのレンダリング処理が実行される。
    catch(:layout_missing) { return render(layout_engine, layout, options, locals) { output } }
  end

  output.extend(ContentTyped).content_type = content_type if content_type
  output
end

compile_templateの中で実行しているのは下記の通り

  • emplate_cache.fetchTilt::Cache#fetchを使ってキャッシュ
  • Tilt[engine]でtemplate engine(今回はerb)を取得
  • find_templateでtemplateファイルのパス(./views/index.erb等)を取得
  • template.new(path, 1, options)レンダリング用のオブジェクトを返却
def compile_template(engine, data, options, views)
  eat_errors = options.delete :eat_errors
  # templateのキャッシュを生成
  template_cache.fetch engine, data, options, views do
    # ここでerbのテンプレートエンジンを取得
    # https://github.com/rtomayko/tilt
    template = Tilt[engine]
    raise "Template engine not found: #{engine}" if template.nil?

    # dataは:index or :layoutになる。
    case data
    when Symbol
      # これらは全部nilになってる。
      body, path, line = settings.templates[data]
      if body
        body = body.call if body.respond_to?(:call)
        template.new(path, line.to_i, options) { body }
      else
        found = false
        @preferred_extension = engine.to_s
        # templateファイルのpathを返却
        # :index => "./views/index.erb"
        # :layout => "./views/layout.erb"
        find_template(views, data, template) do |file|
          path ||= file # keep the initial path rather than the last one
          if found = File.exist?(file)
            path = file
            break
          end
        end
        throw :layout_missing if eat_errors and not found
        # レンダリング用のtemplateオブジェクトを返却
        template.new(path, 1, options)
      end
    when Proc, String
      body = data.is_a?(String) ? Proc.new { data } : data
      caller = settings.caller_locations.first
      path = options[:path] || caller[0]
      line = options[:line] || caller[1]
      template.new(path, line.to_i, options, &body)
    else
      raise ArgumentError, "Sorry, don't know how to render #{data.inspect}."
    end
  end
end

Sinatraでは、rubyの各種template(hamlとかerbとか)を良い感じに扱えるgemを使っているようですね👀

github.com

レスポンスボディへの設定

HTMLテキストの生成されるまでの流れが理解出来たところで最初に戻ってみると、getメソッドの最後にerbが実行されているから最終的なHTMLテキストがgetメソッドの返り値として返却されることがわかります!

require 'sinatra'

get '/' do
  @title = "index"
  @content = "Hello World"
  erb :index
end

これを踏まえてresponceの生成処理を見ていきます。前回の記事の通りsinatraはリクエストを受け取ると#call!が呼ばれ、invoke { dispatch! }が呼ばれます。dispath!の中ではroute!が実行されてroutingの処理(getで定義した処理)が実行されます。

それでinvokecatch(:halt) { yield }dispath!が実行されて、resに返り値の最終的なHTMLテキストが格納されます。

それでbody(res.pop)response.body = valueでレスポンスのボディに最終的なHTMLテキストが設定されるというわけですね 🎉

def invoke
  # dispath!が実行されてされてHTMLテキストが返却される。
  # => "<html>\n<head>\n  <title>index</title>\n</head>\n<body>\n  <h1>Sinatra Sample</h1>\n<p>message:Hello World</p>\n\n\n</body>\n</html>\n"
  res = catch(:halt) { yield }
  res = [res] if Integer === res or String === res
  if Array === res and Integer === res.first
    res = res.dup
    status(res.shift)
    body(res.pop)
    headers(*res)
  elsif res.respond_to? :each
    body res
  end
  nil # avoid double setting the same response tuple twice
end

おわりに

Sinatraのコード読んで、

  • Webサーバーがどうやって起動するか
  • URLにアクセスしてroutingで定義した処理がどう実行されているか
  • viewsに置いたテンプレートとインスタンス変数がどう紐付いて実際のHTMLとして返却されるか

といったことがRubyのコードでどう実現されているかが何となく理解することが出来ました 💪

あとはSinatraチュートリアルを読んで見るとrailsのbefore_actionに相当する機能があったり、もっとシンプルなフレームワークかと思っていたのですが意外と機能があることがわかりました👀

普通に個人で軽くサービス作るならRailsではなくSinatraの方が前提知識も少なくていいし、割と勉強中の方にはいいのでは?という気持ちにもなったのですが、いかんせん情報量的にRailsのほうが圧倒的に多いので難しいところですね💦

Sinatraをよりシンプルにした軽量のWebアプリケーションのフレームワーク作ったらかなり勉強になりそうだったので、時間があったらやってみたいと思いました💪

パフォーマンス測定の心構えとTipsのMEMO

最近、パフォーマンス測定について学びがあったので基本的な内容が多いですが、自戒もこめてまとめておきますm( )m

大事なこと

  • 思い込みを捨てる
    • あたりをつけてそこから測定するのは大事だが、迷ったときにはここでは起きないはずといった思い込みは捨てて、ちゃんと網羅的に測定する。
  • ボトルネック、発生箇所を場所を測定し特定する
    • どこで発生しているのか、ボトルネックはどこなのかをパフォーマンス等を定量的に測り特定する。
  • 条件を合わせる
    • ローカルで同様の処理を実行して試す等を行う際は、実際に発生した状況との乖離をなるべく少なくする。
    • 複数の対策を比較するときは検証方法や前提を揃えて測定する。

思い込みを捨てる

例えばどこかしらの処理でパフォーマンスの起きてる場合には、根拠を持ってあたりを付けて着手する(N+1が発生してそうな処理なので、そこがパフォーマンス劣化の原因になっていそう等)のは時間短縮的にも望ましいと思いますが、道に迷ったときには、全体像を思い浮かべて網羅的・定量的に測定してボトルネックを探すことが大事。

例) リクエストからレスポンスが返却されるまで(サーバー単位)

f:id:madogiwa0124:20190811130920p:plain

例) URL解決から結果が生成されるまで(Rails)

f:id:madogiwa0124:20190811130959p:plain

ボトルネック、発生箇所を場所を測定し特定する

コードを見てもパフォーマンスが遅くなっていそうな場所が見当たらない等の場合はシステムのパフォーマンスを測定して、どのような場所に負荷がかかっているのかを測定すると、どのような処理が影響してそうか判断するヒントになります。(コードではなく他のプロセスがサーバー上の負荷を上げてただけ等の事象にも気づけそうです。)

迷ったときはシステムで大まかに測定し、その結果をもとにコードを見ていくと答えが見つかるかもしれません。

システムのパフォーマンスを測定する

システムのパフォーマンスを測るために使えそうなcommand等のtipsを記載しておきます。

起動中のプロセスを確認する

ps auxコマンドを使うと起動中のすべてのプロセスのCPU使用率やメモリの使用率を確認することが出来ます。これを使うと他のサービスによる影響等が確認できそうです。

$ ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.8 2126072 36208 pts/0   Ssl+ Aug10   0:01 /usr/local/lib/erlang/erts-10.2.2/bin/beam.smp -- -root /usr/local/lib/erlang -progname erl -- -home /root -- -pa /usr/local/lib/elixir/bin
root        29  0.0  0.0   4184   680 ?        Ss   Aug10   0:00 erl_child_setup 1048576
root        48  0.0  0.0  19948  3540 pts/1    Ss+  Aug10   0:00 bash
root        64  0.0  0.0  19948  3704 pts/2    Ss   03:14   0:00 bash
root        84  0.0  0.0  38384  3112 pts/2    R+   03:16   0:00 ps aux

現在のメモリの空き容量を確認する

free -tコマンドを使うと現在のメモリの空き容量をスワップ領域を含めて確認することが出来ます。これを見るとメモリの逼迫によるスワップ領域を使用した事によりパフォーマンスが劣化した可能性があるかどうか確認できそうです。

$ free -t
              total        used        free      shared  buff/cache   available
Mem:        4042484      323724     3371672         804      347088     3504124
Swap:       1048572           0     1048572
Total:      5091056      323724     4420244

システム全体の負荷状況を確認する

topコマンドを使うとシステム全体の負荷状況をリアルタイムに確認することが出来ます。 -o %MEMを指定するとメモリの使用量の降順に並び替えることも出来ます。topコマンドを確認しながら該当処理を実行すると、どのような影響が出ているかを確認できそうです。

$ top -o %MEM
Tasks:   4 total,   1 running,   3 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.0 us,  0.2 sy,  0.0 ni, 99.8 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  4042484 total,  3371332 free,   323748 used,   347404 buff/cache
KiB Swap:  1048572 total,  1048572 free,        0 used.  3504084 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
    1 root      20   0 2126072  36208   5316 S   0.0  0.9   0:00.94 beam.smp
   48 root      20   0   19948   3540   2988 S   0.0  0.1   0:00.04 bash
   63 root      20   0   42776   3432   2972 R   0.3  0.1   0:00.01 top
   29 root      20   0    4184    680    612 S   0.0  0.0   0:00.29 erl_child_setup

dockerの各コンテナの負荷状況を確認する

docker上の各コンテナの負荷状況をリアルタイムに見るにはdocker statsコマンドを使います。コンテナで運用している場合は、リクエストからレスポンスまで時間がかかった際にどのコンテナの負荷が上がっているか測定すると、ボトルネックが特定できそうです。

$ docker stats
CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS
8038defbab1e        elixir-work         0.04%               34.47MiB / 3.855GiB   0.87%               1.25kB / 0B         21MB / 0B           22

※特定のNameを持つコンテナだけ見る場合は$ docker stats | grep コンテナ名

マシンの性能を測る

実際のマシンのパフォーマンスがどの程度か測るにはベンチマークを取るライブラリが便利です。実際に使っているマシンの性能がそもそも足りてないや、環境で差分が出た際にマシンスペックの差かどうかを判断することが出来ます。

例)UNIX BENCHの実行方法

$ wget https://byte-unixbench.googlecode.com/files/UnixBench5.1.3.tgz
$ tar xvf UnixBench5.1.3.tgz
$ cd UnixBench
$ ./Run

参考: UnixBenchでベンチマーク - IDCF テックブログ

コードのパフォーマンスを測定する

実際にどこで発生したかわかってきたら実際にコードのパフォーマンスを測るとより具体的になります。printを使った手法、またrubyrailsには標準でベンチマークを測定出来る機能があります。

標準出力で情報を見ることによって、コードのどの部分がボトルネックになっているのか特定することが出来ます。

今回は下記のようなコードを例に見ていきます。

resources = Hoge.where(fuga: piyo)
objects = resources.map { |resource| fix_process(resource) }
return objects

def fix_process
  # something process
end

標準出力で見る

上記のコードを解析するには下記のような標準出力を仕込めば下記処理について、おおよその実行時間を把握することが出来ます。

puts "=== START ==="
puts "- Hoge.where(fuga: piyo)"
puts Time.now
resources = Hoge.where(fuga: piyo)
p resouces
puts Time.now

puts "- resources.map { |resource| fix_process(resource) }"
puts Time.now
objects = resources.map { |resource| fix_process(resource) }
p resouces
puts Time.now
puts "=== END ==="
return objects

def fix_process
  puts "- fix_process"
  puts Time.now
  # something process
  puts Time.now
end

rubyのbenchmark機能を使う

rubyの標準のベンチマーク機能を使うと下記のようにBenchmark.bmのブロックに渡した処理のベンチマークを取得出来ます。

require 'benchmark'
puts "=== START ==="
Benchmark.bm do |r|
  r("where") { resources = Hoge.where(fuga: piyo) }
  r("map") { objects = resources.map { |resource| fix_process(resource) } }
end
return objects

def fix_process
  Benchmark.bm do |r|
    r("fix") { # something process }
  end
end

これはサンプルですが下記のような結果を得られます。

       user     system      total        real
test  0.000282   0.000478   0.000760 (  5.020090)

ちなみに見方は下記の通りです。

  • user: rubyプログラムの実行時間
  • system: ファイルの読み書き等のシステムコールの実行時間
  • real: 実際の経過時間

参考: Ruby でベンチマークを取る方法 - Qiita

条件を合わせる

ここは言わずもがなですが、なるべくパフォーマンスが劣化した環境と測定時は揃えた方が良いです。劣化時と状況が違ってしまうと何が原因かわからなくなってしまうので。。。

  • 測定する項目(CPU、メモリ、実行時間)
  • 実行時のTBLのレコード件数
  • 処理の呼び出し方(引数等)
  • config周りの設定

などなど...

おわりに

今回はパフォーマンス測定の心構えやTipsをまとめてみました。こういう言語によらないエンジニアとしての基礎知識が自分には不足しているので、学びがあったときはちゃんと整理して身につけられるようにしていきたい・・・!