From b02383a80bf3415ea5bb23b1255f8e1878257658 Mon Sep 17 00:00:00 2001 From: zachdaniel Date: Wed, 2 Jan 2019 15:18:48 -0500 Subject: [PATCH] feat: store average block time in a genserver --- .../controllers/chain_controller.ex | 3 +- .../lib/block_scout_web/notifier.ex | 3 +- .../templates/chain/show.html.eex | 21 ++-- apps/block_scout_web/priv/gettext/default.pot | 24 ++-- .../priv/gettext/en/LC_MESSAGES/default.po | 24 ++-- apps/explorer/config/config.exs | 2 + apps/explorer/config/test.exs | 2 + apps/explorer/lib/explorer/application.ex | 1 + apps/explorer/lib/explorer/chain.ex | 26 ----- .../explorer/counters/average_block_time.ex | 106 ++++++++++++++++++ apps/explorer/mix.exs | 2 +- apps/explorer/test/explorer/chain_test.exs | 14 --- .../counters/average_block_time_test.exs | 64 +++++++++++ 13 files changed, 217 insertions(+), 75 deletions(-) create mode 100644 apps/explorer/lib/explorer/counters/average_block_time.ex create mode 100644 apps/explorer/test/explorer/counters/average_block_time_test.exs diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex index ca16c28978..3658045c5a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex @@ -4,6 +4,7 @@ defmodule BlockScoutWeb.ChainController do alias BlockScoutWeb.ChainView alias Explorer.{Chain, PagingOptions, Repo} alias Explorer.Chain.{Address, Block, Transaction} + alias Explorer.Counters.AverageBlockTime alias Explorer.ExchangeRates.Token alias Explorer.Market alias Phoenix.View @@ -17,7 +18,7 @@ defmodule BlockScoutWeb.ChainController do conn, "show.html", address_count: Chain.count_addresses_with_balance_from_cache(), - average_block_time: Chain.average_block_time(), + average_block_time: AverageBlockTime.average_block_time(), exchange_rate: exchange_rate, chart_data_path: market_history_chart_path(conn, :show), transaction_estimated_count: transaction_estimated_count, diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index df44c47932..1cc3f68daa 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -7,6 +7,7 @@ defmodule BlockScoutWeb.Notifier do alias BlockScoutWeb.Endpoint alias Explorer.{Chain, Market, Repo} alias Explorer.Chain.{Address, InternalTransaction, Transaction} + alias Explorer.Counters.AverageBlockTime alias Explorer.ExchangeRates.Token def handle_event({:chain_event, :addresses, :realtime, addresses}) do @@ -109,7 +110,7 @@ defmodule BlockScoutWeb.Notifier do defp broadcast_block(block) do preloaded_block = Repo.preload(block, [[miner: :names], :transactions, :rewards]) - average_block_time = Chain.average_block_time() + average_block_time = AverageBlockTime.average_block_time(preloaded_block) Endpoint.broadcast("blocks:new_block", "new_block", %{ block: preloaded_block, diff --git a/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex index 9ac079c482..93477260e2 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex @@ -32,14 +32,19 @@
-
- - <%= gettext "Average block time" %> - - - <%= Timex.format_duration(@average_block_time, :humanized) %> - -
+ <%= case @average_block_time do %> + <% {:error, :disabled} -> %> + <%= nil %> + <% average_block_time -> %> +
+ + <%= gettext "Average block time" %> + + + <%= Timex.format_duration(average_block_time, :humanized) %> + +
+ <% end %>
<%= gettext "Total transactions" %> diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index 55b0029041..c6bec2bbb1 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -118,7 +118,7 @@ msgid "All" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/chain/show.html.eex:37 +#: lib/block_scout_web/templates/chain/show.html.eex:41 msgid "Average block time" msgstr "" @@ -175,7 +175,7 @@ msgid "BlockScout provides analytics data, API, and Smart Contract tools for the msgstr "" #, elixir-format -#: lib/block_scout_web/templates/chain/show.html.eex:67 +#: lib/block_scout_web/templates/chain/show.html.eex:72 #: lib/block_scout_web/templates/layout/_topnav.html.eex:16 #: lib/block_scout_web/templates/layout/_topnav.html.eex:20 msgid "Blocks" @@ -572,7 +572,7 @@ msgid "More internal transactions have come in" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/chain/show.html.eex:91 +#: lib/block_scout_web/templates/chain/show.html.eex:96 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:14 #: lib/block_scout_web/templates/transaction/index.html.eex:14 msgid "More transactions have come in" @@ -949,7 +949,7 @@ msgid "Total Supply" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/chain/show.html.eex:45 +#: lib/block_scout_web/templates/chain/show.html.eex:50 msgid "Total transactions" msgstr "" @@ -983,7 +983,7 @@ msgstr "" #: lib/block_scout_web/templates/block_transaction/index.html.eex:23 #: lib/block_scout_web/templates/block_transaction/index.html.eex:26 #: lib/block_scout_web/templates/block_transaction/index.html.eex:35 -#: lib/block_scout_web/templates/chain/show.html.eex:88 +#: lib/block_scout_web/templates/chain/show.html.eex:93 #: lib/block_scout_web/templates/layout/_topnav.html.eex:35 #: lib/block_scout_web/views/address_view.ex:253 msgid "Transactions" @@ -1058,12 +1058,12 @@ msgid "Verify & publish" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/chain/show.html.eex:66 +#: lib/block_scout_web/templates/chain/show.html.eex:71 msgid "View All Blocks →" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/chain/show.html.eex:87 +#: lib/block_scout_web/templates/chain/show.html.eex:92 msgid "View All Transactions →" msgstr "" @@ -1103,7 +1103,7 @@ msgid "WEI" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/chain/show.html.eex:53 +#: lib/block_scout_web/templates/chain/show.html.eex:58 msgid "Wallet addresses" msgstr "" @@ -1187,8 +1187,8 @@ msgstr "" #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:19 #: lib/block_scout_web/templates/address_validation/index.html.eex:63 #: lib/block_scout_web/templates/address_validation/index.html.eex:82 -#: lib/block_scout_web/templates/chain/show.html.eex:79 -#: lib/block_scout_web/templates/chain/show.html.eex:105 +#: lib/block_scout_web/templates/chain/show.html.eex:84 +#: lib/block_scout_web/templates/chain/show.html.eex:110 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:24 msgid "Loading..." msgstr "" @@ -1399,7 +1399,7 @@ msgstr "" #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:26 #: lib/block_scout_web/templates/address_transaction/index.html.eex:55 #: lib/block_scout_web/templates/address_validation/index.html.eex:70 -#: lib/block_scout_web/templates/chain/show.html.eex:71 +#: lib/block_scout_web/templates/chain/show.html.eex:76 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:23 #: lib/block_scout_web/templates/tokens/holder/index.html.eex:22 #: lib/block_scout_web/templates/tokens/transfer/index.html.eex:20 @@ -1522,6 +1522,6 @@ msgid "Emission Contract" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/chain/show.html.eex:97 +#: lib/block_scout_web/templates/chain/show.html.eex:102 msgid "Something went wrong, click to retry." msgstr "" diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po index d4b3799b37..b6048ade9b 100644 --- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po +++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po @@ -118,7 +118,7 @@ msgid "All" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/chain/show.html.eex:37 +#: lib/block_scout_web/templates/chain/show.html.eex:41 msgid "Average block time" msgstr "" @@ -175,7 +175,7 @@ msgid "BlockScout provides analytics data, API, and Smart Contract tools for the msgstr "" #, elixir-format -#: lib/block_scout_web/templates/chain/show.html.eex:67 +#: lib/block_scout_web/templates/chain/show.html.eex:72 #: lib/block_scout_web/templates/layout/_topnav.html.eex:16 #: lib/block_scout_web/templates/layout/_topnav.html.eex:20 msgid "Blocks" @@ -572,7 +572,7 @@ msgid "More internal transactions have come in" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/chain/show.html.eex:91 +#: lib/block_scout_web/templates/chain/show.html.eex:96 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:14 #: lib/block_scout_web/templates/transaction/index.html.eex:14 msgid "More transactions have come in" @@ -949,7 +949,7 @@ msgid "Total Supply" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/chain/show.html.eex:45 +#: lib/block_scout_web/templates/chain/show.html.eex:50 msgid "Total transactions" msgstr "" @@ -983,7 +983,7 @@ msgstr "" #: lib/block_scout_web/templates/block_transaction/index.html.eex:23 #: lib/block_scout_web/templates/block_transaction/index.html.eex:26 #: lib/block_scout_web/templates/block_transaction/index.html.eex:35 -#: lib/block_scout_web/templates/chain/show.html.eex:88 +#: lib/block_scout_web/templates/chain/show.html.eex:93 #: lib/block_scout_web/templates/layout/_topnav.html.eex:35 #: lib/block_scout_web/views/address_view.ex:253 msgid "Transactions" @@ -1058,12 +1058,12 @@ msgid "Verify & publish" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/chain/show.html.eex:66 +#: lib/block_scout_web/templates/chain/show.html.eex:71 msgid "View All Blocks →" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/chain/show.html.eex:87 +#: lib/block_scout_web/templates/chain/show.html.eex:92 msgid "View All Transactions →" msgstr "" @@ -1103,7 +1103,7 @@ msgid "WEI" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/chain/show.html.eex:53 +#: lib/block_scout_web/templates/chain/show.html.eex:58 msgid "Wallet addresses" msgstr "" @@ -1187,8 +1187,8 @@ msgstr "" #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:19 #: lib/block_scout_web/templates/address_validation/index.html.eex:63 #: lib/block_scout_web/templates/address_validation/index.html.eex:82 -#: lib/block_scout_web/templates/chain/show.html.eex:79 -#: lib/block_scout_web/templates/chain/show.html.eex:105 +#: lib/block_scout_web/templates/chain/show.html.eex:84 +#: lib/block_scout_web/templates/chain/show.html.eex:110 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:24 msgid "Loading..." msgstr "" @@ -1399,7 +1399,7 @@ msgstr "" #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:26 #: lib/block_scout_web/templates/address_transaction/index.html.eex:55 #: lib/block_scout_web/templates/address_validation/index.html.eex:70 -#: lib/block_scout_web/templates/chain/show.html.eex:71 +#: lib/block_scout_web/templates/chain/show.html.eex:76 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:23 #: lib/block_scout_web/templates/tokens/holder/index.html.eex:22 #: lib/block_scout_web/templates/tokens/transfer/index.html.eex:20 @@ -1522,6 +1522,6 @@ msgid "Emission Contract" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/chain/show.html.eex:97 +#: lib/block_scout_web/templates/chain/show.html.eex:102 msgid "Something went wrong, click to retry." msgstr "" diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index d4ec6d7a55..00902c24c2 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -11,6 +11,8 @@ config :explorer, coin: System.get_env("COIN") || "POA", token_functions_reader_max_retries: 3 +config :explorer, Explorer.Counters.AverageBlockTime, enabled: true + config :explorer, Explorer.Counters.AddressesWithBalanceCounter, enabled: true, enable_consolidation: true config :explorer, Explorer.ExchangeRates, enabled: true, store: :ets diff --git a/apps/explorer/config/test.exs b/apps/explorer/config/test.exs index 4830347b94..0a717d8b27 100644 --- a/apps/explorer/config/test.exs +++ b/apps/explorer/config/test.exs @@ -15,6 +15,8 @@ config :explorer, Explorer.ExchangeRates, enabled: false, store: :ets config :explorer, Explorer.KnownTokens, enabled: false, store: :ets +config :explorer, Explorer.Counters.AverageBlockTime, enabled: false + config :explorer, Explorer.Counters.AddressesWithBalanceCounter, enabled: false, enable_consolidation: false config :explorer, Explorer.Market.History.Cataloger, enabled: false diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 2ddd78a6f2..db90ab1706 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -37,6 +37,7 @@ defmodule Explorer.Application do configure(Explorer.KnownTokens), configure(Explorer.Market.History.Cataloger), configure(Explorer.Counters.AddressesWithBalanceCounter), + configure(Explorer.Counters.AverageBlockTime), configure(Explorer.Validator.MetadataProcessor) ] |> List.flatten() diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 3a2712f6e0..1e7f9bbd8e 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -44,7 +44,6 @@ defmodule Explorer.Chain do alias Explorer.{PagingOptions, Repo} alias Dataloader.Ecto, as: DataloaderEcto - alias Timex.Duration @default_paging_options %PagingOptions{page_size: 50} @@ -287,31 +286,6 @@ defmodule Explorer.Chain do |> Repo.all() end - @doc """ - The average time it took to mine/validate the last <= 100 `t:Explorer.Chain.Block.t/0` - """ - @spec average_block_time :: %Timex.Duration{} - def average_block_time do - {:ok, %Postgrex.Result{rows: [[%Postgrex.Interval{months: 0, days: days, secs: seconds}]]}} = - SQL.query( - Repo, - """ - SELECT coalesce(avg(difference), interval '0 seconds') - FROM ( - SELECT b.timestamp - lag(b.timestamp) over (order by b.timestamp) as difference - FROM (SELECT * FROM blocks ORDER BY number DESC LIMIT 101) b - LIMIT 100 OFFSET 1 - ) t - """, - [] - ) - - hours = days * 24 - minutes = 0 - microseconds = 0 - Duration.from_clock({hours, minutes, seconds, microseconds}) - end - @doc """ The `t:Explorer.Chain.Address.t/0` `balance` in `unit`. """ diff --git a/apps/explorer/lib/explorer/counters/average_block_time.ex b/apps/explorer/lib/explorer/counters/average_block_time.ex new file mode 100644 index 0000000000..333b10a51e --- /dev/null +++ b/apps/explorer/lib/explorer/counters/average_block_time.ex @@ -0,0 +1,106 @@ +defmodule Explorer.Counters.AverageBlockTime do + use GenServer + + @moduledoc """ + Caches the number of token holders of a token. + """ + + import Ecto.Query, only: [from: 2] + + alias Explorer.Chain.Block + alias Explorer.Repo + alias Timex.Duration + + @doc """ + Starts a process to periodically update the counter of the token holders. + """ + @spec start_link(term()) :: GenServer.on_start() + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + def average_block_time(block \\ nil) do + enabled? = + :explorer + |> Application.fetch_env!(__MODULE__) + |> Keyword.fetch!(:enabled) + + if enabled? do + block = if block, do: {block.number, DateTime.to_unix(block.timestamp)} + GenServer.call(__MODULE__, {:average_block_time, block}) + else + {:error, :disabled} + end + end + + ## Server + @impl true + def init(_) do + timestamps_query = + from(block in Block, + limit: 100, + offset: 1, + order_by: [desc: block.number], + select: {block.number, block.timestamp} + ) + + timestamps = + timestamps_query + |> Repo.all() + |> Enum.map(fn {number, timestamp} -> + {number, DateTime.to_unix(timestamp)} + end) + + {:ok, %{timestamps: timestamps, average: average_distance(timestamps)}} + end + + @impl true + def handle_call({:average_block_time, nil}, _from, %{average: average} = state), do: {:reply, average, state} + + def handle_call({:average_block_time, block}, _from, state) do + state = add_block(state, block) + {:reply, state.average, state} + end + + # This is pretty naive, but we'll only ever be sorting 100 dates so I don't think + # complex logic is really necessary here. + defp add_block(%{timestamps: timestamps} = state, block) do + timestamps = + [block | timestamps] + |> Enum.sort_by(fn {number, _} -> number end, &Kernel.>/2) + |> Enum.take(100) + + %{state | timestamps: timestamps, average: average_distance(timestamps)} + end + + defp average_distance([]), do: Duration.from_seconds(0) + defp average_distance([_]), do: Duration.from_seconds(0) + + defp average_distance(timestamps) do + durations = durations(timestamps) + + {sum, count} = + Enum.reduce(durations, {0, 0}, fn duration, {sum, count} -> + {sum + duration, count + 1} + end) + + average = sum / count + + average + |> round() + |> Duration.from_seconds() + end + + defp durations(timestamps) do + timestamps + |> Enum.reduce({[], nil}, fn {_, timestamp}, {durations, last_timestamp} -> + if last_timestamp do + duration = last_timestamp - timestamp + {[duration | durations], timestamp} + else + {durations, timestamp} + end + end) + |> elem(0) + end +end diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index d6102c987c..f6016be17d 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -112,7 +112,7 @@ defmodule Explorer.Mixfile do {:spandex_ecto, "~> 0.4.0"}, # Attach `:prometheus_ecto` to `:ecto` {:telemetry, "~> 0.2.0"}, - # `Timex.Duration` for `Explorer.Chain.average_block_time/0` + # `Timex.Duration` for `Explorer.Counters.AverageBlockTime.average_block_time/0` {:timex, "~> 3.4"} ] end diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 4954a81ae0..4fe32c603b 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -490,20 +490,6 @@ defmodule Explorer.ChainTest do end end - describe "average_block_time/0" do - test "without blocks duration is 0" do - assert Chain.average_block_time() == Timex.Duration.parse!("PT0S") - end - - test "with blocks is average duration between blocks" do - first_block = insert(:block) - second_block = insert(:block, timestamp: Timex.shift(first_block.timestamp, seconds: 3)) - insert(:block, timestamp: Timex.shift(second_block.timestamp, seconds: 9)) - - assert Chain.average_block_time() == Timex.Duration.parse!("PT6S") - end - end - describe "balance/2" do test "with Address.t with :wei" do assert Chain.balance(%Address{fetched_coin_balance: %Wei{value: Decimal.new(1)}}, :wei) == Decimal.new(1) diff --git a/apps/explorer/test/explorer/counters/average_block_time_test.exs b/apps/explorer/test/explorer/counters/average_block_time_test.exs new file mode 100644 index 0000000000..9efabce384 --- /dev/null +++ b/apps/explorer/test/explorer/counters/average_block_time_test.exs @@ -0,0 +1,64 @@ +defmodule Explorer.Counters.AverageBlockTimeTest do + use Explorer.DataCase + + alias Explorer.Counters.AverageBlockTime + + defp block(number, last, duration), do: %{number: number, timestamp: Timex.shift(last, seconds: duration)} + + setup do + start_supervised!(AverageBlockTime) + Application.put_env(:explorer, AverageBlockTime, enabled: true) + + on_exit(fn -> + Application.put_env(:explorer, AverageBlockTime, enabled: false) + end) + end + + describe "average_block_time/1" do + test "when disabled, it returns an error" do + Application.put_env(:explorer, AverageBlockTime, enabled: false) + + assert AverageBlockTime.average_block_time() == {:error, :disabled} + end + + test "without blocks duration is 0" do + assert AverageBlockTime.average_block_time() == Timex.Duration.parse!("PT0S") + end + + test "with only one block, the duration is 0" do + now = Timex.now() + block = block(0, now, 0) + + assert AverageBlockTime.average_block_time(block) == Timex.Duration.parse!("PT0S") + end + + test "once there are two blocks, the duration is the average distance between them all" do + now = Timex.now() + + block0 = block(0, now, 0) + block1 = block(1, now, 2) + block2 = block(2, now, 6) + + AverageBlockTime.average_block_time(block0) + assert AverageBlockTime.average_block_time(block1) == Timex.Duration.parse!("PT2S") + assert AverageBlockTime.average_block_time(block2) == Timex.Duration.parse!("PT3S") + end + + test "only the last 100 blocks are considered" do + now = Timex.now() + + block0 = block(0, now, 0) + block1 = block(1, now, 2000) + + AverageBlockTime.average_block_time(block0) + AverageBlockTime.average_block_time(block1) + + for i <- 1..100 do + block = block(i + 1, now, 2000 + i) + AverageBlockTime.average_block_time(block) + end + + assert AverageBlockTime.average_block_time() == Timex.Duration.parse!("PT1S") + end + end +end