Raw File
config.ml
(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2022 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.                                                 *)
(*                                                                           *)
(*****************************************************************************)

type t = {namespace : string; config : Data_encoding.json; children : t list}

let empty =
  {namespace = Namespace.(to_string empty); config = `Null; children = []}

(* We will need to assume an arbitrary root, which will represent the empty list of t *)
let encoding : t Data_encoding.t =
  let open Data_encoding in
  mu "config_tree" (fun x ->
      conv
        (function
          | {namespace; config; children} -> (namespace, config, children))
        (fun (namespace, config, children) -> {namespace; config; children})
        (obj3
           (req "namespace" string)
           (req "config" json)
           (req "children" (list (dynamic_size x)))))

(* [merge_on] is a Json merging method used to update the fields of a Json document with new values.
   [ja] contains the basic structure and types of the document. [ja] and [merge_on ja jb] have the
   same structure/can be destructed using the same [Data_encoding.t]. In practice, we use it to sequentially
   aggregate all the configurations along a namespace path in the configuration document, and obtain
   the final configuration used during a given benchmark, and to update a meta config file.
*)
(* We represent json documents as trees with labelled edges and labelled nodes of three kinds:
     - value nodes, with no children, labelled with a type (string, float or bool), and a value of that type
     - array nodes, labelled with their number of children n. The outgoing edges are labelled from 0 to n-1.
     - and object nodes, their outgoing edges are labelled with distinct string values.
   Let A and B be two json documents, v a value node of A, and P(A,v) its path in A. Let f(A) be the modified
   tree from A in which we ignore the values in the value nodes. We say that v "exists" in B if and only if
   the path f(P(A,v)) is a subtree (included) in f(B).
   We have [merge_on A B] = [C], with [C] the only json document such that
   - f(A) = f(C)
   - For each value node v in A,
     - if v exists in B, then P(B,v) is included in C.
     - else, P(A,v) is included in C
*)
let rec merge_on ja jb =
  match (ja, jb) with
  | `String _, `String sb -> `String sb
  | `Float _, `Float f2 -> `Float f2
  | `Bool _, `Bool b2 -> `Bool b2
  | `A l1, `A l2 -> (
      match
        List.map2 ~when_different_lengths:() (fun x y -> merge_on x y) l1 l2
      with
      | Ok l -> `A l
      | Error () -> `A l1)
  | `O l1, `O l2 ->
      let tl =
        List.fold_left
          (fun acc (n, f) ->
            match List.find (fun (x, _) -> String.equal x n) l2 with
            | None -> (n, f) :: acc
            | Some (_, o) -> (n, merge_on f o) :: acc)
          []
          l1
      in
      (* List.rev is just aesthetic, it maintains the order of the object fields.
         Convenient for fields like [min] and [max] *)
      `O (List.rev tl)
  | _ -> ja

let get_config ((module Bench) : Benchmark.t) (config : t) : Data_encoding.json
    =
  let default =
    Data_encoding.Json.construct
      ~include_default_fields:`Always
      Bench.config_encoding
      Bench.default_config
  in
  let rec traversal config_acc name config =
    let config_acc = merge_on config_acc config.config in
    match name with
    | h :: t -> (
        match
          List.find (fun x -> String.equal h x.namespace) config.children
        with
        | None -> config_acc
        | Some c -> traversal config_acc t c)
    | [] -> config_acc
  in
  traversal
    default
    (Namespace.to_list Bench.name)
    {namespace = ""; config = `Null; children = [config]}

(* [get_config_strict name conf] returns the configuration defined exactly at the namespace [name] in [conf].
   We currently call it for when we need to edit [conf] precisely at [name] *)
let get_config_strict (name : Namespace.t) (config_full : t) :
    Data_encoding.json =
  let rec traversal name config =
    match name with
    | h :: t -> (
        match
          List.find (fun x -> String.equal h x.namespace) config.children
        with
        | None -> `Null
        | Some c -> traversal t c)
    | [] -> config.config
  in
  traversal
    (Namespace.to_list name)
    {namespace = ""; config = `Null; children = [config_full]}

let merge_config_trees (dst : t) (src : t) : t =
  let get_child namespace {children; _} : t option =
    List.find (fun x -> String.equal x.namespace namespace) children
  in
  let rec build namespace subtree1 subtree2 =
    let default = {namespace; config = `Null; children = []} in
    let subtree1 = Option.value ~default subtree1 in
    let config =
      match subtree2 with None -> subtree1.config | Some c -> c.config
    in
    let subtree2 = Option.value ~default subtree2 in
    let all_children =
      List.sort_uniq
        String.compare
        (List.map
           (fun x -> x.namespace)
           (subtree1.children @ subtree2.children))
    in
    match all_children with
    | [] -> {namespace; config; children = []}
    | _ ->
        {
          namespace;
          config;
          children =
            List.map
              (fun x -> build x (get_child x subtree1) (get_child x subtree2))
              all_children;
        }
  in
  if not (String.equal dst.namespace ".") then (
    Format.eprintf
      "Failure while merging: root namespace in destination should be \".\" \
       (got %s)@."
      dst.namespace ;
    exit 1)
  else if not (String.equal src.namespace ".") then (
    Format.eprintf
      "Failure while merging: root namespace in source should be \".\" (got \
       %s)@."
      src.namespace ;
    exit 1)
  else build "." (Some dst) (Some src)

let parse_config (type c t) ?print ((module Bench) : (c, t) Benchmark.poly)
    config_file =
  let default_config () =
    Format.eprintf
      "Using default configuration for benchmark %a@."
      Namespace.pp
      Bench.name ;
    Data_encoding.Json.construct Bench.config_encoding Bench.default_config
  in
  let try_load_custom_config path =
    let json =
      match Benchmark_helpers.load_json path with
      | Ok json ->
          Format.eprintf
            "Looking for custom configuration for benchmark %a@."
            Namespace.pp
            Bench.name ;
          Some json
      | Error (Sys_error err) ->
          Format.eprintf "Failed loading json %s (Ignoring)@." err ;
          None
      | Error exn -> raise exn
    in
    json
  in
  let decode encoding json =
    try Data_encoding.Json.destruct encoding json
    with exn ->
      Format.eprintf
        "Json deserialization error:@.  %a@."
        (Data_encoding.Json.print_error ~print_unknown:(fun _ exn -> raise exn))
        exn ;
      exit 1
  in
  match config_file with
  | None ->
      let json = default_config () in
      Format.eprintf "%a@." Data_encoding.Json.pp json ;
      Bench.default_config
  | Some file -> (
      let json = try_load_custom_config file in
      let json =
        match json with
        | None -> default_config ()
        | Some json ->
            let json = decode encoding json in
            Format.eprintf "Config file successfully parsed@." ;
            get_config (module Bench) json
      in
      match json with
      | `Null ->
          let json = default_config () in
          Format.eprintf "%a@." Data_encoding.Json.pp json ;
          Bench.default_config
      | json ->
          let config = decode Bench.config_encoding json in
          Format.eprintf
            "Loaded configuration from %s for benchmark %a@."
            file
            Namespace.pp
            Bench.name ;
          (match print with
          | None -> ()
          | Some out ->
              Format.fprintf
                (Format.formatter_of_out_channel out)
                "%a@."
                Data_encoding.Json.pp
                json) ;
          config)

let rec build_branch config name =
  match name with
  | [namespace] -> {namespace; config; children = []}
  | namespace :: t ->
      {namespace; config = `Null; children = [build_branch config t]}
  | [] -> assert false
(* This [assert false] is not reachable because this function is not exported and only called on
   the result of Namespace.to_list which is never empty. *)

let generate_one (module Bench : Benchmark.S) =
  let config =
    Data_encoding.Json.construct Bench.config_encoding Bench.default_config
  in
  build_branch config (Namespace.to_list Bench.name)

let generate_default bench_list =
  List.fold_left
    (fun acc b -> merge_config_trees acc (generate_one b))
    empty
    bench_list

let merge_config_files ?(delete_src = false) ~(dst : string) ~(src : string) ()
    =
  let dst_path = dst in
  let src_path = src in
  let dst =
    match Benchmark_helpers.load_json dst_path with
    | Ok dst -> dst
    | Error exn -> raise exn
  in
  let src =
    match Benchmark_helpers.load_json src_path with
    | Ok src -> src
    | Error exn -> raise exn
  in
  let dst = Data_encoding.Json.destruct encoding dst in
  let src = Data_encoding.Json.destruct encoding src in
  let rslt = merge_config_trees dst src in
  let rslt_str =
    Data_encoding.Json.construct encoding rslt |> Data_encoding.Json.to_string
  in
  let _nwritten =
    Lwt_main.run @@ Lwt_utils_unix.create_file dst_path rslt_str
  in
  if delete_src then Stdlib.Sys.remove src_path

let edit_config ?(input = `Stdin) config_path namespace =
  match Registration.find_namespace namespace with
  | None ->
      Format.eprintf
        "Config file edition failed: given namespace is not registered.@."
  | Some namespace -> (
      let tmpfile =
        Filename.(concat (get_temp_dir_name ()) "octez-snoop_config_edit")
      in
      let command e = Format.sprintf "%s %s" e tmpfile in
      let try_edit e =
        let ret = Stdlib.Sys.command (command e) in
        if ret != 0 then
          Format.eprintf "Config file edition failed: could not open editor@."
        else ()
      in
      let config =
        match Benchmark_helpers.load_json config_path with
        | Ok c -> Data_encoding.Json.destruct encoding c
        | Error exn -> raise exn
      in
      let base_conf =
        get_config_strict namespace config |> Data_encoding.Json.to_string
      in
      let edited =
        try
          match input with
          | `Edit e -> (
              let _nwritten =
                Lwt_main.run @@ Lwt_utils_unix.create_file tmpfile base_conf
              in
              try_edit e ;
              match Benchmark_helpers.load_json tmpfile with
              | Ok c -> c
              | Error exn -> raise exn)
          | `File i -> (
              match Benchmark_helpers.load_json i with
              | Ok c -> c
              | Error exn -> raise exn)
          | `String s -> Ezjsonm.from_string s
          | `Stdin -> Ezjsonm.from_channel In_channel.stdin
        with Ezjsonm.Parse_error (_, s) ->
          Format.eprintf "Config file edition failed: %s@." s ;
          exit 1
      in
      let new_conf_branch = build_branch edited (Namespace.to_list namespace) in
      let new_conf = merge_config_trees config new_conf_branch in
      let new_conf_str =
        Data_encoding.Json.construct encoding new_conf
        |> Data_encoding.Json.to_string
      in
      let _nwritten =
        Lwt_main.run @@ Lwt_utils_unix.create_file config_path new_conf_str
      in
      match input with `Edit _ -> Stdlib.Sys.remove tmpfile | _ -> ())
back to top