Use index and COUNT(*) for address_to_validation_count

BlockValidationCounter is not needed once the query is switched to
`COUNT(*)` and an index is added for `blocks.miner_hash`.  Creating the
index on eth60-test took only 12 seconds.

```
2018-12-21T08:32:46.375 application=ecto_sql [info] == Running 20181221143000
Explorer.Repo.Migrations.CreateBlocksMinerHashIndex.change/0 forward 2018-12-21T08:32:46.375 application=ecto_sql [info] create index blocks_miner_hash_index
2018-12-21T08:32:59.211 application=ecto_sql [info] == Migrated 20181221143000 in 12.8s
```

Before the index both `COUNT(*)` and `COUNT(hash)` (the old usage before
76fc8e0377 in
92f99cac22) take > 800ms, which would have
a noticable impact on the UI.

```
sql> SELECT COUNT(*)
     FROM blocks
     WHERE blocks.miner_hash = (
       SELECT blocks.miner_hash
       FROM blocks
       LIMIT 1
     )
[2018-12-21 08:27:43] 1 row retrieved starting from 1 in 906 ms (execution: 889 ms, fetching: 17 ms)
sql> SELECT COUNT(hash)
     FROM blocks
     WHERE blocks.miner_hash = (
       SELECT blocks.miner_hash
       FROM blocks
       LIMIT 1
     )
[2018-12-21 08:28:04] 1 row retrieved starting from 1 in 819 ms (execution: 811 ms, fetching: 8 ms)
```

After the index both queries benefited because `COUNT(hash)` was able to
use Bitmap Index Scan, but `COUNT(*)` was much better using a normal
Index Scan:

```
sql> SELECT COUNT(hash)
     FROM blocks
     WHERE blocks.miner_hash = (
       SELECT blocks.miner_hash
       FROM blocks
       LIMIT 1
     )
[2018-12-21 08:33:43] 1 row retrieved starting from 1 in 776 ms
(execution: 768 ms, fetching: 8 ms)
sql> SELECT COUNT(*)
     FROM blocks
     WHERE blocks.miner_hash = (
       SELECT blocks.miner_hash
       FROM blocks
       LIMIT 1
     )
[2018-12-21 08:33:55] 1 row retrieved starting from 1 in 130 ms
(execution: 120 ms, fetching: 10 ms)
```

The `SELECT` to get a miner_hash takes 54ms alone, so the `COUNT(*)`
takes ~70ms, but EXPLAINs take just as long so ~50ms is my latency to
eth60-test, meaning the `SELECT COUNT(*)` takes about 20ms plus latency.
pull/1275/head
Luke Imhoff 6 years ago
parent f67241cfc0
commit 903d322553
  1. 7
      apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs
  2. 6
      apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs
  3. 7
      apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs
  4. 8
      apps/block_scout_web/test/block_scout_web/controllers/address_read_contract_controller_test.exs
  5. 13
      apps/block_scout_web/test/block_scout_web/controllers/address_token_controller_test.exs
  6. 7
      apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs
  7. 4
      apps/block_scout_web/test/block_scout_web/features/address_contract_verification_test.exs
  8. 48
      apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs
  9. 5
      apps/block_scout_web/test/block_scout_web/features/viewing_chain_test.exs
  10. 7
      apps/block_scout_web/test/block_scout_web/features/viewing_transactions_test.exs
  11. 2
      apps/explorer/config/config.exs
  12. 4
      apps/explorer/config/test.exs
  13. 1
      apps/explorer/lib/explorer/application.ex
  14. 5
      apps/explorer/lib/explorer/chain.ex
  15. 115
      apps/explorer/lib/explorer/counters/block_validation_counter.ex
  16. 7
      apps/explorer/priv/repo/migrations/20181221143000_create_blocks_miner_hash_index.exs
  17. 45
      apps/explorer/test/explorer/counters/block_validation_counter_test.exs

@ -1,12 +1,9 @@
defmodule BlockScoutWeb.AddressContractControllerTest do
use BlockScoutWeb.ConnCase,
# ETS table is shared in `Explorer.Counters.BlockValidationCounter`
async: false
use BlockScoutWeb.ConnCase, async: true
import BlockScoutWeb.Router.Helpers, only: [address_contract_path: 3]
alias Explorer.Chain.Hash
alias Explorer.Counters.BlockValidationCounter
alias Explorer.ExchangeRates.Token
alias Explorer.Factory
@ -47,8 +44,6 @@ defmodule BlockScoutWeb.AddressContractControllerTest do
created_contract_address: address
)
start_supervised!(BlockValidationCounter)
conn = get(conn, address_contract_path(BlockScoutWeb.Endpoint, :index, address))
assert html_response(conn, 200)

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.AddressControllerTest do
# ETS tables are shared in `Explorer.Counters.*`
async: false
alias Explorer.Counters.{AddressesWithBalanceCounter, BlockValidationCounter}
alias Explorer.Counters.AddressesWithBalanceCounter
describe "GET index/2" do
test "returns top addresses", %{conn: conn} do
@ -15,8 +15,6 @@ defmodule BlockScoutWeb.AddressControllerTest do
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(BlockValidationCounter)
conn = get(conn, address_path(conn, :index))
assert conn.assigns.address_tx_count_pairs
@ -31,8 +29,6 @@ defmodule BlockScoutWeb.AddressControllerTest do
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(BlockValidationCounter)
conn = get(conn, address_path(conn, :index))
assert html_response(conn, 200) =~ address_name.name

@ -1,13 +1,10 @@
defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do
use BlockScoutWeb.ConnCase,
# ETS table is shared in `Explorer.Counters.BlockValidationCounter`
async: false
use BlockScoutWeb.ConnCase, async: true
import BlockScoutWeb.Router.Helpers,
only: [address_internal_transaction_path: 3, address_internal_transaction_path: 4]
alias Explorer.Chain.{Block, InternalTransaction, Transaction}
alias Explorer.Counters.BlockValidationCounter
alias Explorer.ExchangeRates.Token
describe "GET index/3" do
@ -28,8 +25,6 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do
test "includes USD exchange rate value for address in assigns", %{conn: conn} do
address = insert(:address)
start_supervised!(BlockValidationCounter)
conn = get(conn, address_internal_transaction_path(BlockScoutWeb.Endpoint, :index, address.hash))
assert %Token{} = conn.assigns.exchange_rate

@ -1,9 +1,6 @@
defmodule BlockScoutWeb.AddressReadContractControllerTest do
use BlockScoutWeb.ConnCase,
# ETS table is shared in `Explorer.Counters.BlockValidationCounter`
async: false
use BlockScoutWeb.ConnCase, async: true
alias Explorer.Counters.BlockValidationCounter
alias Explorer.ExchangeRates.Token
describe "GET index/3" do
@ -26,9 +23,6 @@ defmodule BlockScoutWeb.AddressReadContractControllerTest do
transaction = insert(:transaction, from_address: contract_address)
start_supervised!(BlockValidationCounter)
BlockValidationCounter.consolidate_blocks()
insert(
:internal_transaction_create,
index: 0,

@ -1,12 +1,9 @@
defmodule BlockScoutWeb.AddressTokenControllerTest do
use BlockScoutWeb.ConnCase,
# ETS table is shared in `Explorer.Counters.BlockValidationCounter`
async: false
use BlockScoutWeb.ConnCase, async: true
import BlockScoutWeb.Router.Helpers, only: [address_token_path: 3]
alias Explorer.Chain.{Token}
alias Explorer.Counters.BlockValidationCounter
describe "GET index/2" do
test "with invalid address hash", %{conn: conn} do
@ -60,8 +57,6 @@ defmodule BlockScoutWeb.AddressTokenControllerTest do
to_address: address
)
start_supervised!(BlockValidationCounter)
conn = get(conn, address_token_path(conn, :index, address))
actual_token_hashes =
@ -103,8 +98,6 @@ defmodule BlockScoutWeb.AddressTokenControllerTest do
%Token{name: name, type: type, inserted_at: inserted_at} = token
start_supervised!(BlockValidationCounter)
conn =
get(conn, address_token_path(BlockScoutWeb.Endpoint, :index, address.hash), %{
"token_name" => name,
@ -136,8 +129,6 @@ defmodule BlockScoutWeb.AddressTokenControllerTest do
insert(:token_transfer, token_contract_address: token.contract_address, from_address: address)
end)
start_supervised!(BlockValidationCounter)
conn = get(conn, address_token_path(BlockScoutWeb.Endpoint, :index, address.hash))
assert conn.assigns.next_page_params
@ -148,8 +139,6 @@ defmodule BlockScoutWeb.AddressTokenControllerTest do
token = insert(:token)
insert(:token_transfer, token_contract_address: token.contract_address, from_address: address)
start_supervised!(BlockValidationCounter)
conn = get(conn, address_token_path(BlockScoutWeb.Endpoint, :index, address.hash))
refute conn.assigns.next_page_params

@ -1,12 +1,9 @@
defmodule BlockScoutWeb.AddressTransactionControllerTest do
use BlockScoutWeb.ConnCase,
# ETS table is shared in `Explorer.Counters.BlockValidationCounter`
async: false
use BlockScoutWeb.ConnCase, async: true
import BlockScoutWeb.Router.Helpers, only: [address_transaction_path: 3, address_transaction_path: 4]
alias Explorer.Chain.Transaction
alias Explorer.Counters.BlockValidationCounter
alias Explorer.ExchangeRates.Token
describe "GET index/2" do
@ -50,8 +47,6 @@ defmodule BlockScoutWeb.AddressTransactionControllerTest do
test "includes USD exchange rate value for address in assigns", %{conn: conn} do
address = insert(:address)
start_supervised!(BlockValidationCounter)
conn = get(conn, address_transaction_path(BlockScoutWeb.Endpoint, :index, address.hash))
assert %Token{} = conn.assigns.exchange_rate

@ -2,7 +2,6 @@ defmodule BlockScoutWeb.AddressContractVerificationTest do
use BlockScoutWeb.FeatureCase, async: true
alias BlockScoutWeb.{AddressContractPage, ContractVerifyPage}
alias Explorer.Counters.BlockValidationCounter
alias Explorer.Factory
alias Plug.Conn
@ -30,9 +29,6 @@ defmodule BlockScoutWeb.AddressContractVerificationTest do
transaction: transaction
)
start_supervised!(BlockValidationCounter)
BlockValidationCounter.consolidate_blocks()
session
|> AddressContractPage.visit_page(address)
|> AddressContractPage.click_verify_and_publish()

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
# Because ETS tables is shared for `Explorer.Counters.*`
async: false
alias Explorer.Counters.{AddressesWithBalanceCounter, BlockValidationCounter}
alias Explorer.Counters.AddressesWithBalanceCounter
alias BlockScoutWeb.{AddressPage, AddressView, Notifier}
setup do
@ -41,9 +41,6 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
[first_address | _] = addresses
[last_address | _] = Enum.reverse(addresses)
start_supervised!(BlockValidationCounter)
BlockValidationCounter.consolidate_blocks()
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
@ -57,9 +54,6 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
test "viewing address overview information", %{session: session} do
address = insert(:address, fetched_coin_balance: 500)
start_supervised!(BlockValidationCounter)
BlockValidationCounter.consolidate_blocks()
session
|> AddressPage.visit_page(address)
|> assert_text(AddressPage.balance(), "0.0000000000000005 POA")
@ -80,9 +74,6 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
created_contract_address: contract
)
start_supervised!(BlockValidationCounter)
BlockValidationCounter.consolidate_blocks()
address_hash = AddressView.trimmed_hash(address.hash)
transaction_hash = AddressView.trimmed_hash(transaction.hash)
@ -116,9 +107,6 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
created_contract_address: another_contract
)
start_supervised!(BlockValidationCounter)
BlockValidationCounter.consolidate_blocks()
contract_hash = AddressView.trimmed_hash(contract.hash)
transaction_hash = AddressView.trimmed_hash(transaction.hash)
@ -129,13 +117,6 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
end
describe "viewing transactions" do
setup do
start_supervised!(BlockValidationCounter)
BlockValidationCounter.consolidate_blocks()
:ok
end
test "sees all addresses transactions by default", %{
addresses: addresses,
session: session,
@ -214,9 +195,6 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
internal_transaction_lincoln_to_address: internal_transaction,
session: session
} do
start_supervised!(BlockValidationCounter)
BlockValidationCounter.consolidate_blocks()
session
|> AddressPage.visit_page(addresses.lincoln)
|> AddressPage.click_internal_transactions()
@ -230,9 +208,6 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
|> insert(from_address: addresses.lincoln)
|> with_block(insert(:block, number: 7000))
start_supervised!(BlockValidationCounter)
BlockValidationCounter.consolidate_blocks()
session
|> AddressPage.visit_page(addresses.lincoln)
|> AddressPage.click_internal_transactions()
@ -280,9 +255,6 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
token_contract_address: contract_address
)
start_supervised!(BlockValidationCounter)
BlockValidationCounter.consolidate_blocks()
session
|> AddressPage.visit_page(lincoln)
|> assert_has(AddressPage.token_transfers(transaction, count: 1))
@ -324,9 +296,6 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
token_contract_address: contract_address
)
start_supervised!(BlockValidationCounter)
BlockValidationCounter.consolidate_blocks()
session
|> AddressPage.visit_page(morty)
|> assert_has(AddressPage.token_transfers(transaction, count: 1))
@ -361,9 +330,6 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
token_contract_address: contract_address
)
start_supervised!(BlockValidationCounter)
BlockValidationCounter.consolidate_blocks()
session
|> AddressPage.visit_page(lincoln)
|> assert_has(AddressPage.token_transfers(transaction, count: 1))
@ -394,9 +360,6 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
token_contract_address: contract_address
)
start_supervised!(BlockValidationCounter)
BlockValidationCounter.consolidate_blocks()
session
|> AddressPage.visit_page(lincoln)
|> click(AddressPage.token_transfers_expansion(transaction))
@ -431,9 +394,6 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
insert(:address_current_token_balance, address: lincoln, token_contract_address_hash: contract_address.hash)
start_supervised!(BlockValidationCounter)
BlockValidationCounter.consolidate_blocks()
session
|> AddressPage.visit_page(lincoln)
|> AddressPage.click_tokens()
@ -487,9 +447,6 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
insert(:address_current_token_balance, address: lincoln, token_contract_address_hash: contract_address_2.hash)
start_supervised!(BlockValidationCounter)
BlockValidationCounter.consolidate_blocks()
{:ok, lincoln: lincoln}
end
@ -535,9 +492,6 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
insert(:fetched_balance, address_hash: address.hash, value: 5, block_number: block.number)
insert(:fetched_balance, address_hash: address.hash, value: 10, block_number: block_one_day_ago.number)
start_supervised!(BlockValidationCounter)
BlockValidationCounter.consolidate_blocks()
{:ok, address: address}
end

@ -7,7 +7,7 @@ defmodule BlockScoutWeb.ViewingChainTest do
alias BlockScoutWeb.{AddressPage, BlockPage, ChainPage, TransactionPage}
alias Explorer.Chain.Block
alias Explorer.Counters.{AddressesWithBalanceCounter, BlockValidationCounter}
alias Explorer.Counters.AddressesWithBalanceCounter
setup do
Enum.map(401..404, &insert(:block, number: &1))
@ -35,9 +35,6 @@ defmodule BlockScoutWeb.ViewingChainTest do
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(BlockValidationCounter)
BlockValidationCounter.consolidate_blocks()
session
|> ChainPage.visit_page()
|> ChainPage.search(to_string(address.hash))

@ -1,13 +1,10 @@
defmodule BlockScoutWeb.ViewingTransactionsTest do
@moduledoc false
use BlockScoutWeb.FeatureCase,
# ETS tables are shared in `Explorer.Counters.BlockValidationCounter`
async: false
use BlockScoutWeb.FeatureCase, async: true
alias BlockScoutWeb.{AddressPage, TransactionListPage, TransactionLogsPage, TransactionPage}
alias Explorer.Chain.Wei
alias Explorer.Counters.BlockValidationCounter
setup do
block =
@ -142,8 +139,6 @@ defmodule BlockScoutWeb.ViewingTransactionsTest do
session: session,
transaction: transaction
} do
start_supervised!(BlockValidationCounter)
session
|> TransactionLogsPage.visit_page(transaction)
|> TransactionLogsPage.click_address(lincoln)

@ -15,8 +15,6 @@ config :explorer, Explorer.Counters.AddressesWithBalanceCounter, enabled: true,
config :explorer, Explorer.Counters.TokenHoldersCounter, enabled: true, enable_consolidation: true
config :explorer, Explorer.Counters.BlockValidationCounter, enabled: true, enable_consolidation: true
config :explorer, Explorer.ExchangeRates, enabled: true, store: :ets
config :explorer, Explorer.Integrations.EctoLogger, query_time_ms_threshold: :timer.seconds(2)

@ -15,10 +15,6 @@ config :explorer, Explorer.ExchangeRates, enabled: false, store: :ets
config :explorer, Explorer.Counters.AddressesWithBalanceCounter, enabled: false, enable_consolidation: false
config :explorer, Explorer.Counters.BlockValidationCounter, enabled: false, enable_consolidation: true
config :explorer, Explorer.Counters.BlockValidationCounter, enabled: false, enable_consolidation: false
config :explorer, Explorer.Counters.TokenHoldersCounter, enabled: false, enable_consolidation: false
config :explorer, Explorer.Market.History.Cataloger, enabled: false

@ -36,7 +36,6 @@ defmodule Explorer.Application do
configure(Explorer.ExchangeRates),
configure(Explorer.Market.History.Cataloger),
configure(Explorer.Counters.TokenHoldersCounter),
configure(Explorer.Counters.BlockValidationCounter),
configure(Explorer.Counters.AddressesWithBalanceCounter),
configure(Explorer.Validator.MetadataProcessor)
]

@ -44,7 +44,6 @@ defmodule Explorer.Chain do
alias Explorer.Counters.{
AddressesWithBalanceCounter,
BlockValidationCounter,
TokenHoldersCounter
}
@ -988,7 +987,9 @@ defmodule Explorer.Chain do
"""
@spec address_to_validation_count(Address.t()) :: non_neg_integer()
def address_to_validation_count(%Address{hash: hash}) do
BlockValidationCounter.fetch(hash)
query = from(block in Block, where: block.miner_hash == ^hash, select: fragment("COUNT(*)"))
Repo.one(query)
end
@doc """

@ -1,115 +0,0 @@
defmodule Explorer.Counters.BlockValidationCounter do
use GenServer
@moduledoc """
Module responsible for fetching and consolidating the number of
validations from an address.
"""
alias Explorer.Chain
alias Explorer.Chain.Hash
@table :block_validation_counter
def table_name do
@table
end
# It is undesirable to automatically start the consolidation in all environments.
# Consider the test environment: if the consolidation initiates but does not
# finish before a test ends, that test will fail. This way, hundreds of
# tests were failing before disabling the consolidation and the scheduler in
# the test env.
config = Application.get_env(:explorer, Explorer.Counters.BlockValidationCounter)
@enable_consolidation Keyword.get(config, :enable_consolidation)
@doc """
Creates a process to continually monitor the validation counts.
"""
@spec start_link(term()) :: GenServer.on_start()
def start_link(_) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
## Server
@impl true
def init(args) do
create_table()
if enable_consolidation?() do
Task.start_link(&consolidate_blocks/0)
end
Chain.subscribe_to_events(:blocks)
{:ok, args}
end
def create_table do
opts = [
:set,
:named_table,
:public,
read_concurrency: true
]
:ets.new(table_name(), opts)
end
@doc """
Consolidates the number of block validations grouped by `address_hash`.
"""
def consolidate_blocks do
Chain.each_address_block_validation_count(fn {address_hash, total} ->
insert_or_update_counter(address_hash, total)
end)
end
@doc """
Fetches the number of validations related to an `address_hash`.
"""
@spec fetch(Hash.Address.t()) :: non_neg_integer
def fetch(addr_hash) do
do_fetch(:ets.lookup(table_name(), to_string(addr_hash)))
end
defp do_fetch([{_, result} | _]), do: result
defp do_fetch([]), do: 0
@impl true
def handle_info({:chain_event, :blocks, _type, blocks}, state) do
blocks
|> Enum.map(& &1.miner_hash)
|> Enum.each(&insert_or_update_counter(&1, 1))
{:noreply, state}
end
@doc """
Inserts a new item into the `:ets` table.
When the record exist, the counter will be incremented by one. When the
record does not exist, the counter will be inserted with a default value.
"""
@spec insert_or_update_counter(Hash.Address.t(), non_neg_integer) :: term()
def insert_or_update_counter(addr_hash, number) do
string_addr = to_string(addr_hash)
default = {string_addr, 0}
:ets.update_counter(table_name(), string_addr, number, default)
end
@doc """
Returns a boolean that indicates whether consolidation is enabled
In order to choose whether or not to enable the scheduler and the initial
consolidation, change the following Explorer config:
`config :explorer, Explorer.Counters.BlockValidationCounter, enable_consolidation: true`
to:
`config :explorer, Explorer.Counters.BlockValidationCounter, enable_consolidation: false`
"""
def enable_consolidation?, do: @enable_consolidation
end

@ -0,0 +1,7 @@
defmodule Explorer.Repo.Migrations.CreateBlocksMinerHashIndex do
use Ecto.Migration
def change do
create(index(:blocks, [:miner_hash]))
end
end

@ -1,45 +0,0 @@
defmodule Explorer.Counters.BlockValidationCounterTest do
use Explorer.DataCase
alias Explorer.Counters.BlockValidationCounter
setup do
start_supervised!(BlockValidationCounter)
:ok
end
describe "consolidate/0" do
test "loads the address' validations consolidated info" do
address = insert(:address)
insert(:block, miner: address, miner_hash: address.hash)
insert(:block, miner: address, miner_hash: address.hash)
another_address = insert(:address)
insert(:block, miner: another_address, miner_hash: another_address.hash)
BlockValidationCounter.consolidate_blocks()
assert BlockValidationCounter.fetch(address.hash) == 2
assert BlockValidationCounter.fetch(another_address.hash) == 1
end
end
describe "fetch/1" do
test "fetches the total block validations by a given address" do
address = insert(:address)
another_address = insert(:address)
assert BlockValidationCounter.fetch(address.hash) == 0
assert BlockValidationCounter.fetch(another_address.hash) == 0
BlockValidationCounter.insert_or_update_counter(address.hash, 1)
BlockValidationCounter.insert_or_update_counter(another_address.hash, 10)
assert BlockValidationCounter.fetch(address.hash) == 1
assert BlockValidationCounter.fetch(another_address.hash) == 10
end
end
end
Loading…
Cancel
Save