Google Cloud Run + Nginx でティラノスクリプトのゲームをPWAでリリース

はじめに

そこそこ規模の大きいティラノスクリプトで作られたゲームをPWAでリリースしようとした際に起きた現象とその対策について書いた記事です。
内容としてはPWAでリリースすることを考えている人向けの記事になります。

またDockerなどを使っているので、どちらかといえばプログラマー向けの内容になっています。

起きたこと

とある大学から「学生の学習用にティラノスクリプトを使ったゲームを作りたい」と話がありました。
ちなみに、既にある程度の実装などは済んでいて、追加で実装してほしいとのこと。

逐一修正したものをZIP形式でやり取りするのも億劫なので、Netlifyなどのホスティングサービスを使うことにました。
Netilfyとか使えば、GitHubから自動的に最新のアプリをリリースできるので

ただ、実際に動作させると

  • 効果音がずれる
  • 処理がもっさりする

などの問題が発覚。

で、どうしたもんかなぁと考えてました。

原因

原因としては使用しているホスティングサービスのサーバーとの物理的距離の問題とサーバーのスペックの問題のようでした。
ちなみにリロードしてブラウザで表示されるまで15000msとかかかってた……

またブラウザへ表示するまでのレンダリング時間がかなりかかっていることからキャッシュか静的コンテンツを高速に配信することができれば大幅な改善が期待できそうでした。

対策検討

とりあえず処理が重いのはサーバーのスペックと物理的距離のようなので、それらをまず変更することを検討。
また、静的コンテンツを高速に配信してくれると評判のNginxを間にかますことで改善されると考えました。

できればリリースなどはコマンドでサクッとしたいですし、なおかつ使用するときだけ料金が発生するのがいいと考えました。
で、大阪リージョンがあるし、かつコマンドでリリースが楽にできるGoogle Cloud Run がよさそうと思い、そちらを使うことしました。

やったこと

まずは、ティラノスクリプトで作ったゲームのソースコードにDockerfileを以下のように追加します。

FROM nginx:alpine
COPY . /usr/share/nginx/html
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf

EXPOSE $PORT

CMD ["nginx", "-g", "daemon off;"]

COPY . /usr/share/nginx/htmlはティラノスクリプトの静的コンテンツをNginxで処理するディレクトリにコピーしています。

またGoogle Cloud Runで使用するポートが8888固定なのでEXPOSE $PORTで使用するポートをよしなにできるようにしています。
またNginxの設定ファイルもCOPY ./nginx/default.conf /etc/nginx/conf.d/default.confでコピーし、Google Cloud Runで動作するようにしています。

次に、nginxディレクトリをソースコード内に作成します。
nginxディレクトリにdefault.confを以下のように作成します。

server {
    listen       8080;
    server_name  localhost;

    #charset koi8-r;
    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

使用しているポートを変更しているだけですね。

あとはGoogle Cloud Run で使用するコンテナをビルドしてデプロイすればOKです。

# コンテナのビルド
gcloud builds submit --tag gcr.io/<PROJECT-ID>/tyrano

# コンテナをデプロイ
gcloud run deploy --image gcr.io/<PROJECT-ID>/tyrano

これでリリースができました。

結果

Netlifyを使っていた時にリロードしてブラウザで表示されるまで15000msとかかかっていたのが、大体700msくらいまでに抑えることができました!

かなり快適になったので良い感じです。

GitHub Pages + PWAでティラノスクリプトのゲームをスマホで動かしてみた

はじめに

ティラノスクリプトで作ったゲームをPWAにしたい人でGitHub Pagesを使いたい人向けの記事です。

動機

以前、この記事を読んでティラノスクリプトでもPWA対応ができることは知っていたんですが試したことがなかったのでやってみた。

qiita.com

で、PWA対応ができているかを確認するのにいちいちレンタルサーバー借りたりするのも面倒臭かったのでGitHub Pagesで代用してみました。

やったこと

まずはGitHubに適当な名前でリポジトリを作ります。
リポジトリ名はあとで使うので控えておいてください。

次にティラノスクリプトの公式サイトからサンプルのゲームをダウンロードします。
今回は数多くのゲームがリリースされていることを鑑みてv4系列を使用しました。

tyrano.jp

また、PWA対応なのでスタンダードパッケージを使っています。

ダウンロードしてきたサンプルを適当なディレクトリに入れてから git initします。

あとは、manifest.jsonを記事を参考に以下のように作成します。
追加する箇所はindex.htmlと同じ階層です。

{
    "short_name": "SHORTNAME",
    "name": "GAME_TITLE",
    "icons": [
        {
            "src": "link.png",
            "sizes": "144x144",
            "type": "image/png"
        },
        {
            "src": "link_02.png",
            "sizes": "192x192",
            "type": "image/png"
        }
    ],
    "start_url": "/<作成したリポジトリ名>/",
    "display": "fullscreen",
    "background_color": "#000",
    "theme_color": "#fff",
    "orientation": "landscape"
}

一つ注意が必要なのはstart_urlの項目です。

GitHub Pagesでは作成したリポジトリ名でドメインの階層が作られています。
なので/にするとファイルが見つからないことになります。

あとはgit commitとかして、git pushリポジトリにpushすればOKです。

最後に、作成したリポジトリの設定からGitHub Pagesを使うようにすればOKです。

ちなみに実際に作ったものがこちら

リポジトリ:
github.com

PWA対応したゲーム:
s-h-gamelinks.github.io

おわりに

やってみて、GitHub Pagesを使うことで「開発しながらリリース処理もできるのでかなり良いんじゃないかなぁ」と思いました。
特にチームで開発してて、かつ皆Gitを使えるとかであればかなり恩恵はありそう(Issue管理とかやりやすそう)

JavaScriptのexportとimportっぽいものをRubyで書いてみた

はじめに

とある勉強会で以前聞いた「JavaScriptのimportとexportみたいなのがRubyで出来ないか?」という話をもとにRubyでやってみた記事です。

実際のコード

module Klass
  @@klasses = {}
  def export(*klasses)
    klasses.each{
      @@klasses[_1.to_s.to_sym] = _1
    }
  end

  def import
    @@klasses
  end
end

class C1
  def self.value
    42
  end
end

class C2
  def self.value
    21
  end
end

include Klass

export C1, C2

import in { C1: c1, C2: c2 }

p c1.value
# => 42
p c2.value
# => 21

Klassというモジュールの中にexportimportというメソッドをそれぞれ定義しています。
exportメソッドでは複数のクラス(定数)を受け取り、それをクラス変数に保存させています。

で、保存したクラスをimportでメソッドで全て返し、パターンマッチでマッチしたクラスだけを変数として受け取っています。

こんな感じでとりあえず書けそう

ただ、exportでエクスポートしたクラスがどんどん同じ変数に格納されていくのでJavaScriptのそれとはちょっと挙動が違うので意図したものかは微妙かもしれないけど……

Ractor超入門

はじめに

この記事はRuby3で導入される並列・並行処理のための新機能Ractorに興味のある方向けの記事になります。 簡単なRactorを使ったサンプルコードを解説しつつ、理解を深めることができるように書いてみました(ほとんど未来の僕へ向けた記事になっている感じはありますが……)。

また、Ractor自体でどういったことができるのかを調べた結果をまとめた記事でもあります。 そのため後半はRactorで色々遊んだ時のコードをもとに挙動を解説しています。

環境

  • Windows10 2004
  • WSL2(Ubuntu 18.04)
  • ruby 3.0.0dev (2020-09-07T04:29:42Z master 17a27060a7) [x86_64-linux]

Ractorとは?

そもそもRactorについて知らない人もいると思いますので、簡単に紹介します。

RactorとはRuby3で導入される並列・並行処理のための新機能になります。機能自体の提案は数年前からあり、当時はGuildという名前で提案されていました。

しかし、ゲーム業界から「Guildという名前は使っているので、別の名前にしてほしい」という声があり、現在のRactorへと変わりました。

Actorモデルを参考にしているようで、そのためRactor(Ruby’s Actor)という名前に変更されたようです。

Ractorは並行実行の単位であり、それぞれが並行して実行されます。 たとえば以下のコードではputs :helloputs :helloはそれぞれ並行して実行されます。

Ractor.new do
  5.times do
    puts :hello
  end
end

5.times do
    puts :world
end

このコードを実行すると以下のような結果になります。

world
helloworld

hello
world
helloworld

helloworld

hello

このようにそれぞれの処理を並行して実行できます。

またRactorは別のRactorへとオブジェクトを送受信し、同期しながら実行することもできます。同期の方法としてはpush型pull型の二つがあります。

たとえばpush型の場合は以下のようなコードになります。

r1 = Ractor.new do
    :hoge
end

r2 = Ractor.new do
    puts :fuga, Ractor.recv
end

r2.send(r1.take)

r2.take
# => :fuga, :hoge

Ractorではsendメソッドを使い、別のRactorへとオブジェクトを送ることができます。上記のコードだと

r2.send(r1.take)

の部分でr2へと送信しています。

送信されたオブジェクトはRactor内でRactor.recvで受け取ることができ

r2 = Ractor.new do
    puts :fuga, Ractor.recv # 
end

r2.send(r1.take)で送られたオブジェクトを受け取ってputsメソッドに渡すことができます。

またRactorが実行された結果を受け取る際にtakeメソッドを使います。 ですのでr1.take:hogeを受け取っています。

つまり、r2.send(r1.take)ではr1の実行結果を受け取り、それをr2へと送信しています。 そして、r2内のputs :fuga, Ractor.recvputs :fuga, :hogeとなり、fugahogeがそれぞれ出力されるということです。

これがpush型でのオブジェクトをやり取りしている流れになります。

対してpull型は以下のようなコードになります。

r1 = Ractor.new 42 do |arg|
    Ractor.yield arg
end

r2 = Ractor.new r1 do |r1|
    r1.take
end

puts r2.take

Ractor.newで渡した引数は|arg|のようにブロック内で使用できる変数として受け取ることができます。

例えば以下のコードはr1takeメソッドが実行されるまで処理を待ちます。

r1 = Ractor.new 42 do |arg|
    Ractor.yield arg
end

またRactor.newには別のRactorを渡すことができるため以下のように書くことができます。

r2 = Ractor.new r1 do |r1|
    r1.take
end

これでr1が引数として受け取った42r2の中で受け取ることができます。

最後にputs r2.take42を受け取って出力しています。

pull型はこういった流れになります。

ざっくりと解説すると

  • push型: Ractor#send + Ractor.recv
  • pull型: Ractor.yield + Ractor#take

という感じです。

より詳細なRactorの解説に関しては下記のリンクを参照していただければと思います。

Ractorのコード

Ractorの生成

RactorRactor.newでブロックに実行したい処理を書きます。

Ractor.new do
  # このブロックが並行に実行される
end

このブロック内の処理が並行実行されます。

つまり、以下のようなコードの場合

Ractor.new do
    10.times do
        puts :hoge
    end
end

10.times do
    puts :fuga
end

:hoge:fugaがそれぞれ並行に出力されます。

またRactor.newに実行したい処理をブロックとして渡しますので以下のように書くこともできます。

Ractor.new{
    10.times{
        puts :hoge
    }
}

またキーワード引数nameを使って名前を付けることもでき、Ractor#nameで名前をを受け取ることもできます。

r = Ractor.new name: 'r1' do
    puts :hoge
end

p r.name
# => "r1"

これにより、どのRactorで処理が実行されているかを確認することもできそうです。

Ractorへ引数を渡す

Ractor.newに引数を渡すことでブロック内にオブジェクトを渡すことができます。

r = Ractor.new :hoge do |a|
    p a
end

r.take
# => :hoge

このように引数を経由してオブジェクトを渡すことができます。

また複数の引数を渡すこともできます

r = Ractor.new :hoge, :fuga do |a, b|
    p a
    p b
end

r.take
# => fuga
# => hoge

このようにArrayを渡すこともできます。

r = Ractor.new [:hoge, :fuga] do |a|
    p a.inspect
end

r.take
# => "[:hoge, :fuga]"

ちなみに、|a||a, b|に変更すると

r = Ractor.new [:hoge, :fuga] do |a, b|
    p a
    p b
end

r.take
# => :hoge
# => :fuga

という出力結果になります。これはa, b = [:hoge, :fuga]と同じ挙動と解釈されているようです。

またHashの場合は

r = Ractor.new({:hoge => 42, :fuga => 21}) do |a|
    p a
    p a[:hoge]
end

r.take
# => {:hoge=>42, :fuga=>21}
# => 42

と出力されます。 ちなみに、Ractor.newの後に()で括っていないとSyntaxErrorになるので注意が必要です。

r = Ractor.new({:hoge => 42, :fuga => 21}) do |a|
    p a
    p a[:hoge]
end

r.take
# => SyntaxError

Ractorでの返り値

Ractorでは実行されるブロック内の返り値をtakeメソッドで受け取ることができます。

r = Ractor.new do
    :hoge
end

p r.take
# => :hoge

ちなみに、ブロック内でreturnをするとLocalJumpErrorとなるようです。

r = Ractor.new do
    return :fuga
    :hoge
end

p r.take
# => LocalJumpError

Ractor内での例外

Ractor内での例外は以下のようにして受け取ることができます。

r = Ractor.new do
    raise 'error'
end

begin
    r.take
rescue Ractor::RemoteError => e
    p e.message
end

ちなみに、Ractor内でも例外処理を書くことはできます。

r = Ractor.new name: 'r1' do
    begin
        raise 'error'
    rescue => e
        p e.message
    end
end

r.take

またドキュメントによるとRactorのブロック内から返ってきた値を受け取る領域で例外をキャッチできるようです。 つまり以下のようなコードも書くことができます。

r1 = Ractor.new do
    raise 'error'
end

r2 = Ractor.new r1 do |r1|
    begin
        r1.take
    rescue Ractor::RemoteError => e
        p e.message
    end
end 

r2.take
# => "thrown by remote Ractor."

Ractorでの並行実行

簡単な例

こんな感じでRactorで並行実行を行うことができます。

Ractor.new do
    3.times do
        puts 42
    end
end

3.times do
    puts 21
end

実行すると4221という出力がそれぞればらばらに表示されます。

ちょっとした例

以下のような感じで複数のworkerRactorで生成し、それをpipeで経由して値を渡し、結果をまとめることができます。

require 'prime'

pipe = Ractor.new do
  loop do
    Ractor.yield Ractor.recv
  end
end

N = 1000
RN = 10
workers = (1..RN).map do
  Ractor.new pipe do |pipe|
    while n = pipe.take
      Ractor.yield [n, n.prime?]
    end
  end
end

(1..N).each{|i|
  pipe << i
}

pp (1..N).map{
  r, (n, b) = Ractor.select(*workers)
  [n, b]
}.sort_by{|(n, b)| n}
# => 0 ~ 999 までの数値が素数かどうかの結果を出力

このコードでは10個のworkerを生成し、pipeを経由してそれぞれのworkerにオブジェクトを渡しています。 また、受け取ったオブジェクトをRactor.yield [n, n.prime?]で返しています。

こんな感じでworkerを複数作り、pipe経由で処理させたり、結果を受け取ることができます。

よしなにworkerなどを生成して処理させるクラスを書いてみる

先ほどのコードだとworker内の処理が後々で大きくなることもありそうだったので、以下のようにworkerをよしなに生成してくれるクラスを書いてみました。

class Ninsoku
    def initialize(task, worker_count: 10)
      @task = task
      @pipe = create_pipe
      @workers = create_workers(worker_count)
    end

    def send(arg)
        @pipe.send arg
    end

    def run
        yield Ractor.select(*@workers)
    end

    def create_pipe
        Ractor.new do
            loop do
                Ractor.yield Ractor.recv
            end
        end
    end

    def create_workers(worker_count)
        (1..worker_count).map do
            Ractor.new @pipe, @task do |pipe, task|
                loop do 
                  arg = pipe.take
                  task.send arg
                  Ractor.yield task.take
                end
            end
        end
    end
end

Ninsoku.newpipeworkerを生成しています。またtaskは処理させたい内容をRactorで渡し、workerで実行させています。

実際に使うケースとしてはこんな感じです。

task = Ractor.new do
  func = lambda{|n| n.downcase }
  loop do
    Ractor.yield func.call(Ractor.recv)
  end
end

ninsoku = Ninsoku.new(task)

('A'..'Z').each{|i|
  ninsoku.send i
}

('A'..'Z').map{
    ninsoku.run{|r, n|
        puts n
    }
}
# => a ~ z までが並行して出力される

このクラスはあとでgemにでもしてみようかと思います。

gemにしてみました(rubygemsにはpushしていませんが……)

S-H-GAMELINKS/rorker

例えば、僕の住んでいる島根県浜田市AED位置情報をRactorを使って処理してみます。 なお、島根県AED位置情報は島根県が公開しているオープンデータを使用させていただきました。この場を借りて感謝を申し上げます。

島根県 オープンデータカタログサイト

require "rorker"
require "csv"

task = Ractor.new do
  func = lambda{|row| 
    row.map{|value|
      if value =~ /浜田市/
        row
      end  
    }.compact
  }
  loop do
    Ractor.yield func.call(Ractor.recv)
  end
end

rorker = Rorker.new(task)

csv = CSV.read "a.csv"

csv.each do |row|
  rorker.send row
end

n = 0

while n < csv.count
  rorker.run{|worker, result|
    if !result.empty?
      puts result
    end
  }
  n += 1
end

こんな感じで読み取ったCSVを一行づつworkerに渡して並行処理で必要なデータをとってくることもできます。

RactorでNumbered Parameterを使う

Ractorでは処理をブロックで渡しますので、Numbered Parameterを使って引数を受け取ることもできます。

r = Ractor.new :hoge do
    puts _1
end

r.take
# => hoge

ちなみに、複数の引数でも動作します。

r = Ractor.new :hoge, :hoge do
    puts _1
    puts _2
end

r.take
# => hoge
# => fuga

複数渡した場合は渡された順番通りに_1から_9に渡されるみたいです。

ちなみに、Hashを渡した場合はこんな感じになります。

r = Ractor.new ({hoge: 1, fuga: 2}) do
    _1.map do |key, value|
        p ":#{key} => #{value}"
    end
end

r.take
# => ":hoge => 1"
# => ":fuga => 2"

=>を使ったHashも同様の結果になりました

r = Ractor.new({:hoge => 1, :fuga => 2}) do
    _1.map do |key, value|
        p ":#{key} => #{value}"
    end
end

r.take
# => ":hoge => 1"
# => ":fuga => 2"

ただし、Arrayの場合は少し挙動が異なります

r = Ractor.new [1, 2, 3] do
    puts _1
    puts _1.class
    puts _2
    puts _2.class
    puts _3
    puts _3.class    
end

r.take
#=> 1
#=> Integer
#=> 2
#=> Integer
#=> 3
#=> Integer

どうやら通常通り複数の引数を渡した時のようにArrayの先頭から順番に渡されるようです。 おそらくは以下のように解釈されているのではないかと思います。

_1, _2, _3 = [1, 2, 3]

ちなみに、Numbered Parameterで受け取れる数より大きなArrayを渡すと

r = Ractor.new [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] do
    puts _1
    puts _2
    puts _3
    puts _4
    puts _5
    puts _6
    puts _7
    puts _8
    puts _9
end

r.take
#=> 1
#=> 2
#=> 3
#=> 4
#=> 5
#=> 6
#=> 7
#=> 8
#=> 9

このように取得できるのはNumbered Parameterが受け取れる範囲までのようです

Ractor内でNumbered Parameterを使う場合はHashを引数に渡した際か

r = Ractor.new ({hoge: 1, fuga: 2}) do |hash|
    hash.map do
        p ":#{_1} => #{_2}"
    end
end

r.take
":hoge => 1"
":fuga => 2"

またはいくつかの引数を渡した時に省略して書きたいときに使うことになりそうです

r = Ractor.new :hoge, :fuga do
    p _1
    p _2
end

r.take
# => :hoge
# => :fuga

おわりに

この記事を読んでRactorについて興味を持って頂ければ幸いです。 今後もRactorを使って試したコードを追加していこうと思います

参考

Rubyで型宣言っぽくコードを書けるようにしてみた

結論

以下のようなコードが動くようになります。

n = int 42
# => 42が代入される

f = int 4.2
# => TypeError!

やったこと

以下のようにKernelモジュールにモンキーパッチします

module Kernel
    module_function
        
    def int(var = 0)
        if var.is_a?(Integer)
            var
        else
           raise TypeError, "#{var} isn't Integer"
        end
    end 
end

あとは、n = int 42のように書くだけで型宣言っぽくRubyの変数を作ることができます。また異なる型(というかクラス)の値を渡した場合は例外としてTypeErrorが発生します。

n = int 42
i = int 21

p n
# => 42
p i
# => 21

n = int 4.2
# => `int': 4.2 isn't Integer (TypeError)

影響範囲を狭めるのであれば、refinementsで以下のように書けばいいかと

module Type
    refine Kernel do
       module_function
        
       def int(var = 0)
           if var.is_a?(Integer)
               var
           else
              raise TypeError, "#{var} isn't Integer"
           end
       end
    end
end

あとは使いたい箇所でusing TypeとすればOkです

おわりに

とりあえず、IntegerとかStringとかはこんな感じで型宣言っぽく書けそう。ArrayとかHashとかは何かいい書き方ないか考えてみよう

Ractorで複数のオブジェクトを渡せるようにしてみた

結論

Ractorで以下のコードを動くようにしてみました。

r = Ractor.new do
    v1, v2 = Ractor.recv
    puts v1
    puts v2
    puts v1.class
    puts v2.class
end

r.send(1, 2)

r.take
# => 1
# => 2
# => Integer
# => Integer

Ractorって?

Ruby3で導入される並行・並列機能を提供するしくみです。
元々はGuildという名前で数年前から議論されてきたものです。

詳しい話は下記の動画を参照して頂ければと思います。

[JA] Ractor report / Koichi Sasada @ko1

Ractorへオブジェクトを渡す

sendメソッドを使ってRactorへとオブジェクトを渡すことができます。

r = Ractor.new do
    v = Ractor.recv
    puts v
    puts v.class
end

r.send(1)

r.take
# => 1
# => Integer

しかし、以下のように複数のオブジェクトを渡すことはできません。

r = Ractor.new do
    v1, v2 = Ractor.recv
    puts v1
    puts v2
    puts v1.class
    puts v2.class
end

r.send(1, 2)

r.take
# =>wrong number of arguments (given 2, expected 1) (ArgumentError)

ただし配列で渡す分にはOKみたいです。

r = Ractor.new do
    v1, v2 = Ractor.recv
    puts v1
    puts v2
    puts v1.class
    puts v2.class
end

r.send([1, 2])

r.take
# => 1
# => 2
# => Integer
# => Integer

実装を見てみると以下のようになっています(CRubyのソースコード内のractor.rbにて)
現在のsendメソッドは一つのオブジェクトのみを引数に受け取ります。またムーヴして良いかどうかをキーワード引数moveで指定することもできます。

  def send obj, move: false
    __builtin_cexpr! %q{
      ractor_send(ec, RACTOR_PTR(self), obj, move)
    }
  end

__builtin_cexpr!でCの関数を呼び出し、メソッドが受け取った引数をそのままCの関数に渡しています。
余談ですが、最近のCRubyでは内部実装としてRubyの変数をCの関数に渡すようなコードを書くことができるようになっています。

やったこと

Ractorのsendメソッドを以下のように書き替えてみました。

  def send obj, *arg, move: false
    obj = arg.unshift obj unless arg.empty?
    __builtin_cexpr! %q{
      ractor_send(ec, RACTOR_PTR(self), obj, move)
    }
  end

まず、sendメソッドは必ず一つのオブジェクトを引数として受け取っています。その挙動を維持するためにobj, *arg, move: falseのように引数を書き換えています。
またsend(1, 2)のように複数オブジェクトが渡された場合は*argに配列として引数が渡されます。

argが空の配列でない場合、複数オブジェクトが渡されていることになり、最終的にCの関数に渡されるobjを第一引数と可変長引数をマージしたものへと変換しています。

あとは修正したCRubyのソースコードをビルドすればOKです。

これで以下のようにRactorに複数のオブジェクトを渡すことができます。

r = Ractor.new do
    v1, v2 = Ractor.recv
    puts v1
    puts v2
    puts v1.class
    puts v2.class
end

r.send(1, 2)

r.take
# => 1
# => 2
# => Integer
# => Integer

参考

ref: Guild → Ractor
ref: https://github.com/ko1/ruby/blob/ractor/ractor.ja.md
ref: [[JA Ractor report / Koichi Sasada @ko1

追記

ちなみに、モンキーパッチでよければ以下のようにラップしたメソッドを作成すればOKです

class Ractor
    def multi_send(obj, *args, move: true)
        obj = args.unshift obj unless args.empty?
        send(obj, move: move)
    end 
end

r = Ractor.new do
    v1, v2 = Ractor.recv
    puts v1
    puts v2
    puts v1.class
    puts v2.class
end

r.multi_send(1, 2)

r.take

モンキーパッチだと範囲が広いので実務とかで使うならrefinements使った方が影響が少ないのでいいかもしれない。

module RefineRactor
    refine Ractor do
        def multi_send(obj, *args, move: true)
            obj = args.unshift obj unless args.empty?
            send(obj, move: move)
        end 
    end
end

using RefineRactor

r = Ractor.new do
    v1, v2 = Ractor.recv
    puts v1
    puts v2
    puts v1.class
    puts v2.class
end

r.multi_send(1, 2)

r.take

CRubyのFloatクラスの結構なメソッドをbuiltinで書き直してみた

はじめに

Hamada.rbでCRubyのFloatクラスをbuiltinで書き直してみた記事です。どちらかといえば作業ログな内容です。

builtinとは?

過去にいくつか記事にしているので詳細はそちらを見て頂ければと思います

gamelinks007.hatenablog.com
gamelinks007.hatenablog.com
gamelinks007.hatenablog.com

やったこと

とりあえず、お試ししたいだけなので雑にinteger.rbに以下のコードを追加

class Float
  def to_s
    Primitive.attr! 'inline'
    Primitive.cexpr! 'flo_to_s(self)'
  end

  def inspect
    Primitive.attr! 'inline'
    Primitive.cexpr! 'flo_to_s(self)'
  end

  def hash
    Primitive.attr! 'inline'
    Primitive.cexpr! 'flo_hash(self)'
  end

  def to_f
    self
  end

  def abs
    Primitive.attr! 'inline'
    Primitive.cexpr! 'rb_float_abs(self)'
  end

  def magnitude
    Primitive.attr! 'inline'
    Primitive.cexpr! 'rb_float_abs(self)'
  end

  def zero?
    Primitive.attr! 'inline'
    Primitive.cexpr! 'flo_zero_p(self)'
  end

  def to_i
    Primitive.attr! 'inline'
    Primitive.cexpr! 'flo_to_i(self)'
  end

  def to_int
    Primitive.attr! 'inline'
    Primitive.cexpr! 'flo_to_i(self)'
  end

  def nan?
    Primitive.attr! 'inline'
    Primitive.cexpr! 'flo_is_nan_p(self)'
  end

  def infinite?
    Primitive.attr! 'inline'
    Primitive.cexpr! 'rb_flo_is_infinite_p(self)'
  end

  def finite?
    Primitive.attr! 'inline'
    Primitive.cexpr! 'rb_flo_is_finite_p(self)'
  end

  def next_float
    Primitive.attr! 'inline'
    Primitive.cexpr! 'flo_next_float(self)'
  end

  def prev_float
    Primitive.attr! 'inline'
    Primitive.cexpr! 'flo_prev_float(self)'
  end

  def positive?
    Primitive.attr! 'inline'
    Primitive.cexpr! 'flo_positive_p(self)'
  end

  def negative?
    Primitive.attr! 'inline'
    Primitive.cexpr! 'flo_negative_p(self)'
  end
end

次にnumeric.cで以下の部分のコードを削除

    rb_define_method(rb_cFloat, "to_s", flo_to_s, 0);
    rb_define_alias(rb_cFloat, "inspect", "to_s");

    rb_define_method(rb_cFloat, "hash", flo_hash, 0);
    rb_define_method(rb_cFloat, "to_f", flo_to_f, 0);
    rb_define_method(rb_cFloat, "abs", rb_float_abs, 0);
    rb_define_method(rb_cFloat, "magnitude", rb_float_abs, 0);
    rb_define_method(rb_cFloat, "zero?", flo_zero_p, 0);

    rb_define_method(rb_cFloat, "to_i", flo_to_i, 0);
    rb_define_method(rb_cFloat, "to_int", flo_to_i, 0);

    rb_define_method(rb_cFloat, "nan?",      flo_is_nan_p, 0);
    rb_define_method(rb_cFloat, "infinite?", rb_flo_is_infinite_p, 0);
    rb_define_method(rb_cFloat, "finite?",   rb_flo_is_finite_p, 0);
    rb_define_method(rb_cFloat, "next_float", flo_next_float, 0);
    rb_define_method(rb_cFloat, "prev_float", flo_prev_float, 0);
    rb_define_method(rb_cFloat, "positive?", flo_positive_p, 0);
    rb_define_method(rb_cFloat, "negative?", flo_negative_p, 0);

あとはbenchmarkディレクトリ以下にbenchmark.ymlを以下のように作成します

prelude: |
  n = 4.2
benchmark: 
  to_s: |
    n.to_s
  inspect: |
    n.inspect
  hash: |
    n.hash
  to_f: |
    n.to_f
  abs: |
    n.abs
  magnitude: |
    n.magnitude
  zero?: |
    n.zero?
  to_i: |
    n.to_i
  to_int: |
    n.to_int
  nan?: |
    n.nan?
  infinite?: |
    n.infinite?
  finite?: |
    n.finite?
  next_float: |
    n.next_float
  prev_float: |
    n.prev_float
  positive?: |
    n.positive?
  negative?: |
    n.negative?
loop_count: 20000000

最後にmake && make installでビルドして make benchmark/benchmark.yml -e COMPARE_RUBY=~/.rbenv/shims/ruby -e BENCH_RUBY=../install/bin/rubyベンチマークを取ってみた

f:id:gamelinks007:20200908231210p:plain

built-rubyruby 3.0.0dev (2020-09-07T04:29:42Z master 17a27060a7) [x86_64-linux]にさっきのbuiltinをパッチとして当てたもの

だいたい同じくらいかちょっと速いくらいになる模様

知りたかったこと

すこし前にInteger#sizeをbuiltinで書くとちょっと速くなるというチケットを書いた

bugs.ruby-lang.org


で、どうも自分自身のみをレシーバとして受け取るメソッドはbuiltinでちょっと速くなりそうな傾向があるような気がしたので試してみた感じです。

Cでの実装がどうなっているかにもよりそうだけどちょっと速くできるかもしれないことが分かったので良かった。