Revision 1143fc9798ec6e5f58164caffdaca7178402e80b authored by Alain Mebsout on 16 November 2022, 15:55:34 UTC, committed by Alain Mebsout on 18 November 2022, 17:03:24 UTC
1 parent 9bd2352
Raw File
benchmark_tps_command.ml
(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2021 Nomadic Labs, <contact@nomadic-labs.com>               *)
(*                                                                           *)
(* Permission is hereby granted, free of charge, to any person obtaining a   *)
(* copy of this software and associated documentation files (the "Software"),*)
(* to deal in the Software without restriction, including without limitation *)
(* the rights to use, copy, modify, merge, publish, distribute, sublicense,  *)
(* and/or sell copies of the Software, and to permit persons to whom the     *)
(* Software is furnished to do so, subject to the following conditions:      *)
(*                                                                           *)
(* The above copyright notice and this permission notice shall be included   *)
(* in all copies or substantial portions of the Software.                    *)
(*                                                                           *)
(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)
(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,  *)
(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL   *)
(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)
(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING   *)
(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER       *)
(* DEALINGS IN THE SOFTWARE.                                                 *)
(*                                                                           *)
(*****************************************************************************)

open Constants

(** The level at which the benchmark starts. We wait till level 3 because we
   need to inject transactions that target already decided blocks. In
   Tenderbake, a block is decided when there are 2 blocks on top of it. We
   cannot target genesis (level 0) because it is not yet in the right
   protocol, thus we wait till level 1 is decided, i.e. we want level 3. *)
let benchmark_starting_level = 3

(** Look up the minimal block delay given [protocol] and [protocol_constants]. *)
let get_minimal_block_delay protocol protocol_constants =
  let json =
    JSON.parse_file
      (Protocol.parameter_file ~constants:protocol_constants protocol)
  in
  int_of_float JSON.(json |-> "minimal_block_delay" |> as_float)

(** Set max prechecked manager operations count. *)
let set_max_prechecked_manager_operations n client =
  let path = ["chains"; "main"; "mempool"; "filter"] in
  let data =
    `O [("max_prechecked_manager_operations", `Float (Float.of_int n))]
  in
  let* _ = Client.rpc ~data POST path client in
  Lwt.return_unit

(** Get a list of hashes of the given number of most recent blocks. *)
let get_blocks blocks_total client =
  let open Lwt_syntax in
  let path = ["chains"; "main"; "blocks"] in
  let query_string = [("length", Int.to_string blocks_total)] in
  let+ json = Client.rpc ~query_string GET path client in
  List.map JSON.as_string (JSON.as_list (JSON.geti 0 json))

(** Get the total number of injected transactions. *)
let get_total_injected_transactions () =
  let tmp_dir = Filename.get_temp_dir_name () in

  (* Choose the most recently created file. *)
  let operations_file =
    Sys.readdir tmp_dir |> Array.to_list
    |> List.filter_map (fun file ->
           if
             String.has_prefix
               ~prefix:"client-stresstest-injected_operations"
               file
             && Filename.extension file = ".json"
           then
             let f = Filename.concat tmp_dir file in
             Some (Unix.stat f, f)
           else None)
    |> List.sort (fun (a1, _) (b1, _) -> Unix.(compare b1.st_ctime a1.st_ctime))
    |> List.map snd |> List.hd
  in

  match operations_file with
  | None ->
      Format.kasprintf
        Stdlib.failwith
        "The injected operations json file was not found. It should have been \
         generated by the octez-client stresstest command@."
  | Some f ->
      let block_transactions = JSON.as_list (JSON.parse_file f) in

      List.fold_left
        (fun a b ->
          match
            List.assoc ~equal:String.equal "operation_hashes" (JSON.as_object b)
          with
          | Some v -> List.length (JSON.as_list v) + a
          | None -> a)
        0
        block_transactions

(** Get the number of applied transactions in the block with the given
    hash. *)
let get_total_applied_transactions_for_block block client =
  (*
    N.B. Grouping of the operations by validation passes:
    - 0: consensus
    - 1: governance (voting)
    - 2: anonymous (denounciations)
    - 3: manager operations

    We are interested in 3, so we select that.
   *)
  let open Lwt_syntax in
  let path = ["chains"; "main"; "blocks"; block; "operation_hashes"; "3"] in
  let+ json = Client.rpc GET path client in
  List.length (JSON.as_list json)

(** The entry point of the benchmark. *)
let run_benchmark ~lift_protocol_limits ~provided_tps_of_injection ~blocks_total
    ~average_block_path () =
  let open Lwt_syntax in
  (* We run the gas tps estimation in a separate network first in order to
     figure out just how many accounts we need for the benchmark. *)
  let* gas_tps_estimation_results =
    Gas_tps_command.estimate_gas_tps ~average_block_path ()
  in
  Log.info "Tezos TPS benchmark" ;
  Log.info "Protocol: %s" (Protocol.name protocol) ;
  Log.info "Blocks to bake: %d" blocks_total ;
  let* parameter_file =
    Protocol.write_parameter_file
      ~base:(Either.right (protocol, Some protocol_constants))
      (if lift_protocol_limits then
       [
         (["hard_gas_limit_per_block"], `String_of_int 2147483647);
         (["hard_gas_limit_per_operation"], `String_of_int 2147483647);
       ]
      else [])
  in
  (* It is important to use a good estimate of max possible TPS that is
     theoretically achievable. If we send operations with lower TPS than
     this we risk obtaining a sub-estimated value for TPS. If we use a
     higher TPS than the maximal possible we risk to saturate the mempool
     and again obtain a less-than-perfect estimation in the end. *)
  let target_tps_of_injection =
    match provided_tps_of_injection with
    | Some tps_value -> tps_value
    | None ->
        if lift_protocol_limits then Constants.lifted_limits_tps
        else
          Gas.deduce_tps
            ~protocol
            ~protocol_constants
            ~average_transaction_cost:
              gas_tps_estimation_results.average_transaction_cost
            ()
  in
  let round0_duration = get_minimal_block_delay protocol protocol_constants in
  (* We need as many accounts as operations in a block. *)
  let total_bootstraps = target_tps_of_injection * round0_duration in
  let additional_bootstrap_account_count =
    total_bootstraps - Constants.default_bootstraps_count
  in
  Log.info "Accounts to use: %d" total_bootstraps ;
  Log.info "Spinning up the network..." ;
  let regular_transaction_fee, regular_transaction_gas_limit =
    Gas.deduce_fee_and_gas_limit
      gas_tps_estimation_results.transaction_costs.regular
  in
  let smart_contract_parameters =
    Gas.calculate_smart_contract_parameters
      gas_tps_estimation_results.average_block
      gas_tps_estimation_results.transaction_costs
  in
  let max_single_transaction_fee =
    List.fold_left
      max
      (Tez.to_mutez regular_transaction_fee)
      (List.map
         (fun (_, x) -> Tez.to_mutez x.Client.invocation_fee)
         smart_contract_parameters)
  in
  (* We want to give the extra bootstraps as little as possible, just enough
     to do their job. *)
  let default_accounts_balance =
    (max_single_transaction_fee + Constants.gas_safety_margin) * blocks_total
  in
  let* node, client =
    Client.init_with_protocol
      ~nodes_args:Node.[Connections 0; Synchronisation_threshold 0]
      ~parameter_file
      ~timestamp:Now
      ~additional_bootstrap_account_count
      ~default_accounts_balance
      `Client
      ~protocol
      ()
  in
  (* Unknown smart contracts will fail the benchmark anyway, but later and less
     gracefully. Here we try to do it the nice way. *)
  let* () =
    Average_block.check_for_unknown_smart_contracts
      gas_tps_estimation_results.average_block
  in
  (* Use only the default bootstraps as delegates. The baker doesn't like
     to have tens of thousands of delegates passed to it. Luckily, if we
     give very little Tez to the extra bootstraps, the default bootstraps will
     dominate in terms of stake. *)
  let delegates = make_delegates Constants.default_bootstraps_count in
  let _baker = Baker.init ~protocol ~delegates node client in
  Log.info "Originating smart contracts" ;
  let* () =
    Client.stresstest_originate_smart_contracts originating_bootstrap client
  in
  let* () = set_max_prechecked_manager_operations total_bootstraps client in
  Log.info "Waiting to reach the next level" ;
  let* _ = Node.wait_for_level node (benchmark_starting_level - 1) in
  Log.info "Using the parameter file: %s" parameter_file ;
  Log.info "Waiting to reach level %d" benchmark_starting_level ;
  let* _ = Node.wait_for_level node benchmark_starting_level in
  let bench_start = Unix.gettimeofday () in
  Log.info "The benchmark has been started" ;
  let client_stresstest_process =
    Client.spawn_stresstest
      ~fee:regular_transaction_fee
      ~gas_limit:regular_transaction_gas_limit
      ~tps:target_tps_of_injection
        (* The stresstest command allows a small probability of creating
           new accounts along the way. We do not want that, so we set it to
           0. *)
      ~fresh_probability:0.0
      ~smart_contract_parameters
      ~source_aliases:(make_delegates Constants.default_bootstraps_count)
        (* It is essential not to pass all accounts via aliases because every
           alias has to be normalized and that's an extra call of the client
           per account. This does not scale well. On the other hand, if we
           pass Account.key list directly, the stresstest command can use it
           right away. *)
      ~source_accounts:(Client.additional_bootstraps client)
      client
  in
  let* _level =
    Node.wait_for_level node (benchmark_starting_level + blocks_total)
  in
  Process.terminate client_stresstest_process ;
  let* _ = Process.wait client_stresstest_process in
  let bench_end = Unix.gettimeofday () in
  let bench_duration = bench_end -. bench_start in
  Log.info "Produced %d block(s) in %.2f seconds" blocks_total bench_duration ;
  let* produced_block_hashes = get_blocks blocks_total client in
  let total_injected_transactions = get_total_injected_transactions () in
  let total_applied_transactions = ref 0 in
  let handle_one_block block_hash =
    let+ applied_transactions =
      get_total_applied_transactions_for_block block_hash client
    in
    total_applied_transactions :=
      !total_applied_transactions + applied_transactions ;
    Log.info "%s -> %d" block_hash applied_transactions
  in
  let* () = List.iter_s handle_one_block (List.rev produced_block_hashes) in
  Log.info "Total applied transactions: %d" !total_applied_transactions ;
  Log.info "Total injected transactions: %d" total_injected_transactions ;
  let empirical_tps =
    Float.of_int !total_applied_transactions /. bench_duration
  in
  let de_facto_tps_of_injection =
    Float.of_int total_injected_transactions /. bench_duration
  in
  Log.info "TPS of injection (target): %d" target_tps_of_injection ;
  Log.info "TPS of injection (de facto): %.2f" de_facto_tps_of_injection ;
  Log.info "Empirical TPS: %.2f" empirical_tps ;
  let* () = Node.kill node in
  return (de_facto_tps_of_injection, empirical_tps)

let regression_handling defacto_tps_of_injection empirical_tps
    lifted_protocol_limits ~previous_count =
  let lifted_protocol_limits_tag = string_of_bool lifted_protocol_limits in
  let save_and_check =
    Long_test.measure_and_check_regression
      ~previous_count
      ~minimum_previous_count:previous_count
      ~stddev:false
      ~repeat:1
      ~tags:[(Dashboard.Tag.lifted_protocol_limits, lifted_protocol_limits_tag)]
  in
  let* () =
    save_and_check Dashboard.Measurement.defacto_tps_of_injection @@ fun () ->
    defacto_tps_of_injection
  in
  save_and_check Dashboard.Measurement.empirical_tps @@ fun () -> empirical_tps

let register () =
  Long_test.register
    ~__FILE__
    ~title:Dashboard.Test.benchmark_tps
    ~tags:[Dashboard.Test.benchmark_tps]
    ~timeout:(Long_test.Minutes 60)
    ~executors:Long_test.[x86_executor1]
    (fun () ->
      let lift_protocol_limits =
        Cli.get_bool ~default:false "lift-protocol-limits"
      in
      let provided_tps_of_injection =
        Cli.get
          ~default:None
          (fun s ->
            match int_of_string_opt s with
            | None -> None
            | Some x -> Some (Some x))
          "provided_tps_of_injection"
      in
      let blocks_total = Cli.get_int ~default:10 "blocks-total" in
      let average_block_path =
        Cli.get ~default:None (fun s -> Some (Some s)) "average-block"
      in
      let previous_count =
        Cli.get_int ~default:10 "regression-previous-sample-count"
      in
      let* defacto_tps_of_injection, empirical_tps =
        run_benchmark
          ~lift_protocol_limits
          ~provided_tps_of_injection
          ~blocks_total
          ~average_block_path
          ()
      in
      regression_handling
        defacto_tps_of_injection
        empirical_tps
        lift_protocol_limits
        ~previous_count)
back to top