diff --git a/.gitignore b/.gitignore index ee611fe..b5ed1b7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ /deps erl_crash.dump *.ez -/docs \ No newline at end of file +/docs +/bench/snapshots +.env diff --git a/bench/README.md b/bench/README.md new file mode 100644 index 0000000..e0d0571 --- /dev/null +++ b/bench/README.md @@ -0,0 +1,17 @@ +Note about these benchmarks: + +The `stream_process_bench.exs` script uses Meck to stub out the +`ExTwitter.API.Streaming.parse_tweet` function for benchmark isolation. + +However, benchfella does not currently support teardown phases to undo this +stub after the completion of the bench. + +Therefore, if you run them all via the default `mix bench` task (which loads and +runs all benchmarks in this directory), other benchmarks which depend on that +call will get unexpected results. + +For now, just run each benchmark script you want individually directly, e.g. + + mix bench bench/stream_parse_bench.exs + +Sorry for the inconvenience! diff --git a/bench/bench_helper.exs b/bench/bench_helper.exs new file mode 100644 index 0000000..2d2d903 --- /dev/null +++ b/bench/bench_helper.exs @@ -0,0 +1 @@ +Benchfella.start() diff --git a/bench/deserialize_bench.exs b/bench/deserialize_bench.exs new file mode 100644 index 0000000..a7b30c3 --- /dev/null +++ b/bench/deserialize_bench.exs @@ -0,0 +1,22 @@ +defmodule ExTwitter.JSON.Bench do + use Benchfella + @mock_tweet_json File.read!("fixture/mocks/tweet.json") + + bench "decode" do + ExTwitter.JSON.decode(@mock_tweet_json) + end +end + +defmodule ExTwitter.Parser.Bench do + use Benchfella + @mock_tweet_json File.read!("fixture/mocks/tweet.json") + + bench "parse_tweet", [tweet: decode_json()] do + ExTwitter.Parser.parse_tweet(tweet) + end + + defp decode_json do + {:ok, tweet} = ExTwitter.JSON.decode(@mock_tweet_json) + tweet + end +end diff --git a/bench/stream_parse_bench.exs b/bench/stream_parse_bench.exs new file mode 100644 index 0000000..7e34f2c --- /dev/null +++ b/bench/stream_parse_bench.exs @@ -0,0 +1,16 @@ +defmodule ExTwitter.API.Streaming.Parse.Bench do + use Benchfella + import ExTwitter.API.Streaming + + @mock_tweet_json File.read!("fixture/mocks/tweet.json") + @mock_limit_json File.read!("fixture/mocks/limit.json") + + bench "parse_tweet_message(tweet)" do + parse_tweet_message(@mock_tweet_json, receive_messages: true) + end + + bench "parse_tweet_message(control)" do + parse_tweet_message(@mock_limit_json, receive_messages: true) + end + +end diff --git a/bench/stream_process_bench.exs b/bench/stream_process_bench.exs new file mode 100644 index 0000000..0733664 --- /dev/null +++ b/bench/stream_process_bench.exs @@ -0,0 +1,77 @@ +defmodule ExTwitter.API.Streaming.Process.Bench do + use Benchfella + import ExTwitter.API.Streaming + + @mock_tweet_json File.read!("fixture/mocks/tweet.json") + + bench "process stream - single chunk", [req_id: make_ref, m: stub_tweet_parsing!] do + streamProcessor = spawn_stream_processor(self, req_id) + send streamProcessor, {:http, {req_id, :stream, @mock_tweet_json}} + receive_processed_msgs() + end + + bench "process stream - multi chunk", [req_id: make_ref, parts: msg_chunks, m: stub_tweet_parsing!] do + streamProcessor = spawn_stream_processor(self, req_id) + Enum.each(parts, fn part -> + send streamProcessor, {:http, {req_id, :stream, part}} + end) + receive_processed_msgs() + end + + defp spawn_stream_processor(receiver, req_id) do + spawn(fn -> + process_stream(receiver, req_id, []) + end) + end + + # block until we receive a message from the stream reader, raise exception + # if something unexpected happens + defp receive_processed_msgs do + receive do + :parsed -> :ok #mock parsed msg + {:stream, _} -> :ok #real parsed msg + _msg -> raise "got unexpected message back from stream processor!" + after 1000 + -> raise "timed out waiting for stream processor!" + end + end + + # stub out tweet parsing so it doesnt affect benchmark time + # this is a fairly bad way to do it, but benchfella doesnt support a global + # "setup" phase just yet. + # + # also note that since benchfella doesn't support "after" callbacks to unload + # stubs, this stays loaded, so we all benchmarks in this file should be + # considered as using the stub. + defp stub_tweet_parsing! do + try do + :meck.validate(ExTwitter.API.Streaming) + rescue + ErlangError -> + Mix.Shell.IO.info """ + WARNING! We just stubbed ExTwitter.API.Streaming.parse_tweet_message + This stub will be effect until this process ends. + + If you are running multiple benchmarks, these ones should be run + independently of others, you can specify the files to run directly via + `mix bench bench/filename_bench.exs` etc. + """ + :meck.new(ExTwitter.API.Streaming, [:passthrough, :no_history]) + :meck.expect( + ExTwitter.API.Streaming, :parse_tweet_message, + fn(_,_) -> :parsed end + ) + end + end + + # fake chunked messages to emulate chunked network traffic + defp msg_chunks, do: chunkify(@mock_tweet_json, 20) + defp chunkify(msg, n) do + chars = String.graphemes(msg) + chunksize = trunc(length(chars)/n)+1 + + Enum.chunk(chars, chunksize, chunksize, []) + |> Enum.map(&Enum.join/1) + end + +end diff --git a/fixture/mocks/deleted_tweet.json b/fixture/mocks/deleted_tweet.json new file mode 100644 index 0000000..2894d92 --- /dev/null +++ b/fixture/mocks/deleted_tweet.json @@ -0,0 +1 @@ +{"delete":{"status":{"id":1234,"id_str":"1234","user_id":3,"user_id_str":"3"}}} diff --git a/fixture/mocks/limit.json b/fixture/mocks/limit.json new file mode 100644 index 0000000..2bb2999 --- /dev/null +++ b/fixture/mocks/limit.json @@ -0,0 +1 @@ +{"limit":{"timestamp_ms":"1415022747749","track":542}} diff --git a/fixture/mocks/stall_warning.json b/fixture/mocks/stall_warning.json new file mode 100644 index 0000000..4273bae --- /dev/null +++ b/fixture/mocks/stall_warning.json @@ -0,0 +1 @@ +{"warning":{"code":"FALLING_BEHIND","message":"Your connection is falling behind and messages are being queued for delivery to you. Your queue is now over 60% full. You will be disconnected when the queue is full.","percent_full":60}} diff --git a/fixture/mocks/tweet.json b/fixture/mocks/tweet.json new file mode 100644 index 0000000..0585d89 --- /dev/null +++ b/fixture/mocks/tweet.json @@ -0,0 +1 @@ +{"created_at":"Wed Mar 19 16:53:03 +0000 2014","id":446328507694845952,"id_str":"446328507694845952","text":"sample tweet text","source":"web","truncated":false,"in_reply_to_status_id":446225539796594688,"in_reply_to_status_id_str":"446225539796594688","in_reply_to_user_id":86202207,"in_reply_to_user_id_str":"86202207","in_reply_to_screen_name":"suranyami","user":{"id":507309896,"id_str":"507309896","name":"Elixir Lang","screen_name":"elixirlang","location":"","description":"The Elixir programming language that runs on the Erlang VM","url":"http:\/\/t.co\/xGmHND9luN","entities":{"url":{"urls":[{"url":"http:\/\/t.co\/xGmHND9luN","expanded_url":"http:\/\/elixir-lang.org","display_url":"elixir-lang.org","indices":[0,22]}]},"description":{"urls":[]}},"protected":false,"followers_count":2408,"friends_count":6,"listed_count":68,"created_at":"Tue Feb 28 12:31:32 +0000 2012","favourites_count":31,"utc_offset":3600,"time_zone":"Amsterdam","geo_enabled":false,"verified":false,"statuses_count":370,"lang":"en","contributors_enabled":false,"is_translator":false,"is_translation_enabled":false,"profile_background_color":"131516","profile_background_image_url":"http:\/\/abs.twimg.com\/images\/themes\/theme14\/bg.gif","profile_background_image_url_https":"https:\/\/abs.twimg.com\/images\/themes\/theme14\/bg.gif","profile_background_tile":true,"profile_image_url":"http:\/\/pbs.twimg.com\/profile_images\/1859982753\/drop_normal.png","profile_image_url_https":"https:\/\/pbs.twimg.com\/profile_images\/1859982753\/drop_normal.png","profile_link_color":"009999","profile_sidebar_border_color":"EEEEEE","profile_sidebar_fill_color":"EFEFEF","profile_text_color":"333333","profile_use_background_image":true,"default_profile":false,"default_profile_image":false,"following":false,"follow_request_sent":false,"notifications":false},"geo":null,"coordinates":null,"place":null,"contributors":null,"retweet_count":0,"favorite_count":0,"entities":{"hashtags":[],"symbols":[],"urls":[],"user_mentions":[{"screen_name":"suranyami","name":"Ravey Day-V Gravy","id":86202207,"id_str":"86202207","indices":[0,10]}]},"favorited":false,"retweeted":false,"lang":"en"} diff --git a/lib/extwitter/api/streaming.ex b/lib/extwitter/api/streaming.ex index fa76832..d26520e 100644 --- a/lib/extwitter/api/streaming.ex +++ b/lib/extwitter/api/streaming.ex @@ -88,7 +88,8 @@ defmodule ExTwitter.API.Streaming do end end - defp process_stream(processor, request_id, configs, acc \\ []) do + @doc false + def process_stream(processor, request_id, configs, acc \\ []) do receive do {:http, {request_id, :stream_start, headers}} -> send processor, {:header, headers} @@ -102,7 +103,7 @@ defmodule ExTwitter.API.Streaming do is_end_of_message(part) -> message = Enum.reverse([part|acc]) |> Enum.join("") - |> parse_tweet_message(configs) + |> __MODULE__.parse_tweet_message(configs) if message do send processor, message end @@ -127,7 +128,8 @@ defmodule ExTwitter.API.Streaming do defp is_empty_message(part), do: part == "\r\n" defp is_end_of_message(part), do: part =~ ~r/\r\n$/ - defp parse_tweet_message(json, configs) do + @doc false + def parse_tweet_message(json, configs) do try do case ExTwitter.JSON.decode(json) do {:ok, tweet} -> diff --git a/mix.exs b/mix.exs index 61dc23d..65b4ee3 100644 --- a/mix.exs +++ b/mix.exs @@ -34,7 +34,8 @@ defmodule ExTwitter.Mixfile do {:mock, github: "parroty/mock", only: [:dev, :test], branch: "fix"}, {:ex_doc, "~> 0.6", only: :docs}, {:earmark, "~> 0.1", only: :docs}, - {:inch_ex, "~> 0.2", only: :docs} + {:inch_ex, "~> 0.2", only: :docs}, + {:benchfella, github: "alco/benchfella", only: :dev} ] end diff --git a/mix.lock b/mix.lock index 9bdeef7..0c26096 100644 --- a/mix.lock +++ b/mix.lock @@ -1,4 +1,5 @@ -%{"earmark": {:hex, :earmark, "0.1.12"}, +%{"benchfella": {:git, "git://github.com/alco/benchfella.git", "0fd092f2b20bd1b80fda95eb347ae37919f66ec2", []}, + "earmark": {:hex, :earmark, "0.1.12"}, "ex_doc": {:hex, :ex_doc, "0.6.2"}, "exactor": {:hex, :exactor, "0.7.0"}, "excoveralls": {:hex, :excoveralls, "0.3.6"}, diff --git a/test/extwitter_stream_test.exs b/test/extwitter_stream_test.exs index f15c2d7..de1d86f 100644 --- a/test/extwitter_stream_test.exs +++ b/test/extwitter_stream_test.exs @@ -2,10 +2,10 @@ defmodule ExTwitterStreamTest do use ExUnit.Case, async: false import Mock - @mock_tweet_json "{\"created_at\":\"Wed Mar 19 16:53:03 +0000 2014\",\"id\":446328507694845952,\"id_str\":\"446328507694845952\",\"text\":\"sample tweet text\",\"source\":\"web\",\"truncated\":false,\"in_reply_to_status_id\":446225539796594688,\"in_reply_to_status_id_str\":\"446225539796594688\",\"in_reply_to_user_id\":86202207,\"in_reply_to_user_id_str\":\"86202207\",\"in_reply_to_screen_name\":\"suranyami\",\"user\":{\"id\":507309896,\"id_str\":\"507309896\",\"name\":\"Elixir Lang\",\"screen_name\":\"elixirlang\",\"location\":\"\",\"description\":\"The Elixir programming language that runs on the Erlang VM\",\"url\":\"http:\\/\\/t.co\\/xGmHND9luN\",\"entities\":{\"url\":{\"urls\":[{\"url\":\"http:\\/\\/t.co\\/xGmHND9luN\",\"expanded_url\":\"http:\\/\\/elixir-lang.org\",\"display_url\":\"elixir-lang.org\",\"indices\":[0,22]}]},\"description\":{\"urls\":[]}},\"protected\":false,\"followers_count\":2408,\"friends_count\":6,\"listed_count\":68,\"created_at\":\"Tue Feb 28 12:31:32 +0000 2012\",\"favourites_count\":31,\"utc_offset\":3600,\"time_zone\":\"Amsterdam\",\"geo_enabled\":false,\"verified\":false,\"statuses_count\":370,\"lang\":\"en\",\"contributors_enabled\":false,\"is_translator\":false,\"is_translation_enabled\":false,\"profile_background_color\":\"131516\",\"profile_background_image_url\":\"http:\\/\\/abs.twimg.com\\/images\\/themes\\/theme14\\/bg.gif\",\"profile_background_image_url_https\":\"https:\\/\\/abs.twimg.com\\/images\\/themes\\/theme14\\/bg.gif\",\"profile_background_tile\":true,\"profile_image_url\":\"http:\\/\\/pbs.twimg.com\\/profile_images\\/1859982753\\/drop_normal.png\",\"profile_image_url_https\":\"https:\\/\\/pbs.twimg.com\\/profile_images\\/1859982753\\/drop_normal.png\",\"profile_link_color\":\"009999\",\"profile_sidebar_border_color\":\"EEEEEE\",\"profile_sidebar_fill_color\":\"EFEFEF\",\"profile_text_color\":\"333333\",\"profile_use_background_image\":true,\"default_profile\":false,\"default_profile_image\":false,\"following\":false,\"follow_request_sent\":false,\"notifications\":false},\"geo\":null,\"coordinates\":null,\"place\":null,\"contributors\":null,\"retweet_count\":0,\"favorite_count\":0,\"entities\":{\"hashtags\":[],\"symbols\":[],\"urls\":[],\"user_mentions\":[{\"screen_name\":\"suranyami\",\"name\":\"Ravey Day-V Gravy\",\"id\":86202207,\"id_str\":\"86202207\",\"indices\":[0,10]}]},\"favorited\":false,\"retweeted\":false,\"lang\":\"en\"}\r\n" - @mock_limit_json "{\"limit\":{\"timestamp_ms\":\"1415022747749\",\"track\":542}}\r\n" - @mock_deleted_tweet "{\"delete\":{\"status\":{\"id\":1234,\"id_str\":\"1234\",\"user_id\":3,\"user_id_str\":\"3\"}}}\r\n" - @mock_stall_warning "{\"warning\":{\"code\":\"FALLING_BEHIND\",\"message\":\"Your connection is falling behind and messages are being queued for delivery to you. Your queue is now over 60% full. You will be disconnected when the queue is full.\",\"percent_full\":60}}\r\n" + @mock_tweet_json File.read!("fixture/mocks/tweet.json") + @mock_limit_json File.read!("fixture/mocks/limit.json") + @mock_deleted_tweet File.read!("fixture/mocks/deleted_tweet.json") + @mock_stall_warning File.read!("fixture/mocks/stall_warning.json") setup_all do ExVCR.Config.filter_url_params(true) @@ -115,4 +115,3 @@ defmodule ExTwitterStreamTest do send pid, {:http, {request_id, :stream, json}} end end -