Linux CXL
 help / color / mirror / Atom feed
From: "Verma, Vishal L" <vishal.l.verma@intel.com>
To: "sunfishho12@gmail.com" <sunfishho12@gmail.com>,
	"linux-cxl@vger.kernel.org" <linux-cxl@vger.kernel.org>
Cc: "Williams, Dan J" <dan.j.williams@intel.com>,
	"dave@stgolabs.net" <dave@stgolabs.net>,
	"a.manzanares@samsung.com" <a.manzanares@samsung.com>
Subject: Re: [ndctl PATCH 2/2] cxl: Add list image, image-from-file to CXL command
Date: Fri, 9 Sep 2022 22:59:46 +0000	[thread overview]
Message-ID: <093ebf126ad44920d9c7bfd1c30af5040e0c7020.camel@intel.com> (raw)
In-Reply-To: <b3efd743b296b3ef1d10363b505006bad8c1e7ef.1660895649.git.sunfishho12@gmail.com>

On Fri, 2022-08-19 at 01:57 -0700, sunfishho12@gmail.com wrote:
> From: Matthew Ho <sunfishho12@gmail.com>
> 
> This adds the option of creating a diagram of a CXL topology.
> Use cxl list -vvv --image --output-file=topology.png to 
> generate a diagram in topology.png.
> 
> Likewise, use cxl list --image-from-file=topology-txt
> to generate a diagram using topology.txt, where 
> topology.txt was generated by cxl list -vvv beforehand.
> 
> This addition makes it easier to visualize the CXL topology.
> Also, being able to choose to use an input text file 
> allows one to visualize the CXL topology while not having 
> access to CXL hardware, which may be convenient.
> 
> Signed-off-by: Matthew Ho <sunfishho12@gmail.com>
> ---
>  Documentation/cxl/cxl-list.txt |  16 ++
>  cxl/filter.c                   | 279 ++++++++++++++++++++++++++++++++-
>  cxl/filter.h                   |   7 +
>  cxl/list.c                     |  24 +++
>  cxl/meson.build                |   1 +
>  meson.build                    |   1 +
>  6 files changed, 327 insertions(+), 1 deletion(-)
> 
> diff --git a/Documentation/cxl/cxl-list.txt b/Documentation/cxl/cxl-list.txt
> index 698f53b7a9e9..bd39889bbe3a 100644
> --- a/Documentation/cxl/cxl-list.txt
> +++ b/Documentation/cxl/cxl-list.txt
> @@ -354,6 +354,22 @@ OPTIONS
>           Everything *-vv* provides, plus enable
>           --health and --partition.
>  
> +--image::
> +-o::
> +--output-file::

So listing the three this way makes it seem they're the same option
when they are not. I suggest doing the options this way instead:

-o::
--output=::
      Output to a file. Format is decided based on file extension.
      .jpg / .png / .svg: output an image in the specified format with
      the CXL topology.
      .txt / .log / no extension: output a text file containing the
      JSON output
      Any other extension - reserved / unsupported.
      
And with this, drop the --image option entirely, as it can be deduced from
the output format.

> +       Create an image displaying the topology. One can specify an
> +       output with -o or --output-file, which supports outputting
> +       with file types .png, .jpg, or .jpeg. If -o is not specified,
> +       cxl list --image will by default create a new file topology.jpg
> +       to output to.
> +
> +--image-from-file::

Similarly this can just be --input=<file>
You can clarify in the description that the input file needs to be a
plaintext file only, i.e. image-to-text won't be supported.

> +       This subcommand has the same function as --image, except it
> +       requires an input text file to be specified that contains a
> +       json array outputted by cxl list. As with --image, one can
> +       use -o or --output-file to specify an output file, with the
> +       default behavior being to create and output to topology.jpg.
> +
>  --debug::
>         If the cxl tool was built with debug enabled, turn on debug
>         messages.
> diff --git a/cxl/filter.c b/cxl/filter.c
> index 38ece5528794..0ebf62960336 100644
> --- a/cxl/filter.c
> +++ b/cxl/filter.c
> @@ -8,6 +8,7 @@
>  #include <util/json.h>
>  #include <cxl/libcxl.h>
>  #include <json-c/json.h>
> +#include <graphviz/gvc.h>
>  
>  #include "filter.h"
>  #include "json.h"
> @@ -1004,6 +1005,279 @@ walk_children:
>         }
>  }
>  
> +static char *find_device_type(struct json_object *device)
> +{
> +       char *value;
> +
> +       json_object_object_foreach(device, property, value_json) {
> +               value = (char *)json_object_get_string(value_json);
> +               if (!strcmp(property, "bus") &&
> +                   !strcmp(value, "root0"))
> +                       return "ACPI0017 Device";
> +               if (!strcmp(property, "host") &&
> +                   !strncmp(value, "ACPI0016", strlen("ACPI0016")))
> +                       return "Host Bridge";

Might be good to add detection for the "cxl_test" bus here, currently
that shows "unknown device".

> +               if (!strcmp(property, "host"))
> +                       return "Switch Port";
> +               if (!strcmp(property, "memdev"))
> +                       return "Type 3 Memory Device";
> +               if (!strcmp(property, "dport"))
> +                       return "dport";
> +               if (!strcmp(property, "decoder"))
> +                       return "decoder";
> +       }
> +       return "unknown device";
> +}
> +
> +static bool check_device_type(struct json_object *device, char *type)
> +{
> +       return !strcmp(find_device_type(device), type);
> +}
> +
> +/* for labeling purposes */
> +static const char *find_device_ID(struct json_object *device)
> +{
> +       char *dev_type = find_device_type(device);
> +       json_object *ID = json_object_new_string("unknown");
> +
> +       if (!strcmp(dev_type, "ACPI0017 Device")) {
> +               json_object_put(ID);
> +               json_object_object_get_ex(device, "bus", &ID);
> +       }
> +
> +       if (!strcmp(dev_type, "Host Bridge") || !strcmp(dev_type, "Switch Port")) {
> +               json_object_put(ID);
> +               json_object_object_get_ex(device, "host", &ID);
> +       }
> +
> +       if (!strcmp(dev_type, "Type 3 Memory Device")) {
> +               json_object_put(ID);
> +               json_object_object_get_ex(device, "memdev", &ID);
> +       }
> +
> +       if (!strcmp(dev_type, "dport")) {
> +               json_object_put(ID);
> +               json_object_object_get_ex(device, "dport", &ID);
> +       }
> +
> +       return json_object_get_string(ID);
> +}
> +
> +static bool is_device(struct json_object *device)
> +{
> +       char *dev_type = find_device_type(device);
> +
> +       return (strcmp(dev_type, "dport") &&
> +               strcmp(dev_type, "decoder"));
> +}
> +
> +static char *label_device(struct json_object *device)
> +{
> +       char *label;
> +       const char *ID = find_device_ID(device);
> +       const char *devname = find_device_type(device);
> +
> +       asprintf(&label, "%s\nID: %s", devname, ID);
> +       if (!label)
> +               printf("Error: label allocation failed in %s\n",
> +                       __func__);
> +       return label;
> +}
> +
> +/* for determining number of devices listed in a json array */
> +static size_t count_top_devices(struct json_object *top_array)
> +{
> +       size_t dev_counter = 0;
> +       size_t top_array_len = json_object_array_length(top_array);
> +
> +       for (size_t idx = 0; idx < top_array_len; idx++)
> +               if (is_device(json_object_array_get_idx(top_array, idx)))
> +                       dev_counter++;
> +       return dev_counter;
> +}
> +
> +static void create_root_ports(struct json_object *host_bridge, Agraph_t *graph,
> +                      Agnode_t *hb)
> +{
> +       json_object *rps, *rp, *id_json;
> +       char *id, *rp_label;
> +       Agnode_t *rp_node;
> +       size_t nr_dports, idx;
> +
> +       assert(check_device_type(host_bridge, "Host Bridge"));
> +
> +       if (!json_object_object_get_ex(host_bridge, "dports", &rps))
> +               return;
> +
> +       nr_dports = json_object_array_length(rps);
> +
> +       for (idx = 0; idx < nr_dports; idx++) {
> +               rp = json_object_array_get_idx(rps, idx);
> +               json_object_object_get_ex(rp, "dport", &id_json);
> +               id = (char *)json_object_get_string(id_json);
> +               asprintf(&rp_label, "Root Port\nID: %s", id);
> +               if (!rp_label)
> +                       printf("Error: label allocation failed when creating root port nodes\n");
> +               rp_node = agnode(graph, rp_label, 1);
> +               agedge(graph, hb, rp_node, 0, 1);
> +               free(rp_label);
> +       }
> +}
> +
> +static const char *find_root_port(struct json_object *device)
> +{
> +       json_object *rp;
> +
> +       if (json_object_object_get_ex(device, "root port", &rp))
> +               return json_object_get_string(rp);
> +       return NULL;
> +}
> +
> +static char *find_rp_label(struct json_object *device)
> +{
> +       char *rp_node_name;
> +       const char *id = find_root_port(device);
> +
> +       if (!id)
> +               return NULL;
> +       asprintf(&rp_node_name, "Root Port\nID: %s", id);
> +       if (!rp_node_name)
> +               printf("Error: asprintf failed in %s\n", __func__);
> +       return rp_node_name;
> +}
> +
> +static Agnode_t **draw_subtree(struct json_object *current_array,
> +                              Agraph_t *graph)
> +{
> +       size_t json_array_len, nr_top_devices, obj_idx;
> +       size_t idx, nr_sub_devs, nr_devs_connected;
> +       char *label, *rp_label;
> +       Agnode_t **top_devices, **sub_devs, *parent_node;
> +       bool is_hb;
> +       json_object *device, *subdev_arr, *subdev;
> +       json_object_iter subdev_iter;
> +
> +       json_array_len = json_object_array_length(current_array);
> +       nr_top_devices = count_top_devices(current_array);
> +
> +       if (!nr_top_devices)
> +               return NULL;
> +
> +       top_devices = malloc(nr_top_devices * sizeof(device));
> +       if (!top_devices)
> +               printf("Error: Unable to allocate memory for top_devices\n");
> +
> +       for (obj_idx = 0; obj_idx < json_array_len; obj_idx++) {
> +               device = json_object_array_get_idx(current_array,
> +                                                  obj_idx);
> +               if (!is_device(device))
> +                       continue;
> +
> +               label = label_device(device);
> +               top_devices[obj_idx] = agnode(graph, label, 1);
> +
> +               agsafeset(top_devices[obj_idx], "shape", "box", "");
> +
> +               is_hb = check_device_type(device, "Host Bridge");
> +
> +               /* Create root port nodes if device is a host bridge */
> +               if (is_hb)
> +                       create_root_ports(device, graph,
> +                                         top_devices[obj_idx]);
> +
> +               free(label);
> +
> +               json_object_object_foreachC(device, subdev_iter) {
> +                       subdev_arr = subdev_iter.val;
> +
> +                       if (!json_object_is_type(subdev_arr, json_type_array))
> +                               continue;
> +                       nr_sub_devs = count_top_devices(subdev_arr);
> +                       sub_devs = draw_subtree(subdev_arr, graph);
> +                       if (!sub_devs)
> +                               continue;
> +                       if (!is_hb) {
> +                               for (idx = 0; idx < nr_sub_devs; idx++)
> +                                       agedge(graph, top_devices[obj_idx],
> +                                              sub_devs[idx], 0, 1);
> +                               free(sub_devs);
> +                               continue;
> +                       }
> +
> +                       nr_devs_connected = 0;
> +
> +                       for (idx = 0;
> +                            idx < json_object_array_length(subdev_arr);
> +                            idx++) {
> +                               subdev = json_object_array_get_idx(subdev_arr, idx);
> +                               if (!is_device(subdev))
> +                                       continue;
> +
> +                               rp_label = find_rp_label(subdev);
> +                               if (!rp_label) {
> +                                       printf("Error: please use a cxl list output containing root port attributes\n");
> +                                       return NULL;
> +                               }
> +                               parent_node = agnode(graph, rp_label, 0);
> +                               agedge(graph, parent_node,
> +                                      sub_devs[nr_devs_connected], 0, 1);
> +                               free(rp_label);
> +                               nr_devs_connected++;
> +                       }
> +                       free(sub_devs);
> +               }
> +       }
> +       return top_devices;
> +}
> +
> +struct json_object *parse_json_text(const char *path)
> +{
> +       FILE *fp;
> +       char *json_as_string;
> +       size_t file_len;
> +       json_object *json;
> +
> +       fp = fopen(path, "r");
> +       if (!fp)
> +               printf("Error: could not read file\n");
> +       fseek(fp, 0, SEEK_END);
> +       file_len = ftell(fp);
> +       fseek(fp, 0, SEEK_SET);
> +       json_as_string = malloc(file_len + 1);
> +       if (!json_as_string ||
> +           fread(json_as_string, 1, file_len, fp) != file_len) {
> +               free(json_as_string);
> +               printf("Error: could not read file %s\n", path);
> +       }
> +       json_as_string[file_len] = '\0';
> +       json = json_tokener_parse(json_as_string);
> +       return json;
> +}
> +
> +void create_image(const char *filename, json_object *platform)
> +{
> +       char *output_file = filename ? (char *)filename : "topology.jpg";
> +       GVC_t *gvc = gvContext();
> +       Agraph_t *graph = agopen("graph", Agdirected, 0);
> +       char *of_extension = strrchr(output_file, '.') + 1;
> +       Agnode_t **top_devices;
> +
> +       if (!of_extension || (strcmp(of_extension, "png") &&
> +                             strcmp(of_extension, "jpeg") &&
> +                             strcmp(of_extension, "jpg"))) {
> +               printf("Error: unacceptable output file type, please use .png, .jpeg, or .jpg");
> +               return;
> +       }
> +       top_devices = draw_subtree(platform, graph);
> +       free(top_devices);
> +       gvLayout(gvc, graph, "dot");
> +       /* Legality of the file extension is checked in list.c */
> +       gvRender(gvc, graph, strrchr(output_file, '.') + 1,
> +                fopen(output_file, "w"));
> +       gvFreeLayout(gvc, graph);
> +       agclose(graph);
> +}
> +

It might be nice to split all the graphviz interfacing helpers into a
separate graph.c file.

>  int cxl_filter_walk(struct cxl_ctx *ctx, struct cxl_filter_params *p)
>  {
>         struct json_object *jdevs = NULL, *jbuses = NULL, *jports = NULL;
> @@ -1209,7 +1483,10 @@ walk_children:
>                      top_level_objs > 1);
>         splice_array(p, jregions, jplatform, "regions", top_level_objs > 1);
>  
> -       util_display_json_array(stdout, jplatform, flags);
> +       if (p->image)
> +               create_image(p->output_file, jplatform);
> +       else
> +               util_display_json_array(stdout, jplatform, flags);

It might be nice to have the stdout output present unconditionally -
just duplicate it in the output file if requested, either as image or
text, but otherwise keep the usual stdout.

>  
>         return 0;
>  err:
> diff --git a/cxl/filter.h b/cxl/filter.h
> index 256df49c3d0c..aec7207aa885 100644
> --- a/cxl/filter.h
> +++ b/cxl/filter.h
> @@ -5,6 +5,8 @@
>  
>  #include <stdbool.h>
>  #include <util/log.h>
> +#include <json-c/json.h>
> +#include <graphviz/gvc.h>
>  
>  struct cxl_filter_params {
>         const char *memdev_filter;
> @@ -14,6 +16,8 @@ struct cxl_filter_params {
>         const char *endpoint_filter;
>         const char *decoder_filter;
>         const char *region_filter;
> +       const char *image_input_file;
> +       const char *output_file;
>         bool single;
>         bool endpoints;
>         bool decoders;
> @@ -26,6 +30,7 @@ struct cxl_filter_params {
>         bool human;
>         bool health;
>         bool partition;
> +       bool image;
>         int verbose;
>         struct log_ctx ctx;
>  };
> @@ -38,6 +43,8 @@ struct cxl_port *util_cxl_port_filter_by_memdev(struct cxl_port *port,
>                                                 const char *serial);
>  struct cxl_decoder *util_cxl_decoder_filter(struct cxl_decoder *decoder,
>                                             const char *__ident);
> +struct json_object *parse_json_text(const char *path);
> +void create_image(const char *filename, json_object *platform);
>  struct cxl_region *util_cxl_region_filter(struct cxl_region *region,
>                                           const char *__ident);
>  
> diff --git a/cxl/list.c b/cxl/list.c
> index 8c48fbbaaec3..3cbc0de26b2b 100644
> --- a/cxl/list.c
> +++ b/cxl/list.c
> @@ -52,6 +52,13 @@ static const struct option options[] = {
>                     "include memory device health information"),
>         OPT_BOOLEAN('I', "partition", &param.partition,
>                     "include memory device partition information"),
> +       OPT_BOOLEAN(0, "image", &param.image,
> +                  "display CXL topology"),
> +       OPT_STRING(0, "image-from-file", &param.image_input_file,
> +                  "input file path for image",
> +                  "path to file containing a json array describing the topology"),
> +       OPT_STRING('o', "output-file", &param.output_file, "output file path",
> +                  "path to file to output diagram to"),
>         OPT_INCR('v', "verbose", &param.verbose,
>                  "increase output detail"),
>  #ifdef ENABLE_DEBUG
> @@ -74,6 +81,9 @@ int cmd_list(int argc, const char **argv, struct cxl_ctx *ctx)
>         };
>         int i;
>  
> +       json_object *platform;
> +       const char *output_file_name;
> +
>         argc = parse_options(argc, argv, options, u, 0);
>         for (i = 0; i < argc; i++)
>                 error("unknown parameter \"%s\"\n", argv[i]);
> @@ -86,6 +96,20 @@ int cmd_list(int argc, const char **argv, struct cxl_ctx *ctx)
>                 usage_with_options(u, options);
>         }
>  
> +       if (param.output_file)
> +               output_file_name = param.output_file;
> +
> +       if (param.image_input_file) {
> +               if (access(param.image_input_file, R_OK)) {
> +                       error("input file path is incorrect\n");
> +                       return 0;
> +               }
> +               platform = parse_json_text(param.image_input_file);
> +               create_image(output_file_name, platform);
> +               return 0;
> +       }
> +
> +
>         if (num_list_flags() == 0) {
>                 if (param.memdev_filter || param.serial_filter)
>                         param.memdevs = true;
> diff --git a/cxl/meson.build b/cxl/meson.build
> index f2474aaa6e2e..2670d1e6e330 100644
> --- a/cxl/meson.build
> +++ b/cxl/meson.build
> @@ -19,6 +19,7 @@ cxl_tool = executable('cxl',
>      kmod,
>      json,
>      versiondep,
> +    graphviz,
>    ],
>    install : true,
>    install_dir : rootbindir,
> diff --git a/meson.build b/meson.build
> index aecf461fc9ef..3c4f4fc006c3 100644
> --- a/meson.build
> +++ b/meson.build
> @@ -141,6 +141,7 @@ kmod = dependency('libkmod')
>  libudev = dependency('libudev')
>  uuid = dependency('uuid')
>  json = dependency('json-c')
> +graphviz = dependency('libgvc')
>  if get_option('docs').enabled()
>    if get_option('asciidoctor').enabled()
>      asciidoc = find_program('asciidoctor', required : true)


  reply	other threads:[~2022-09-09 23:00 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-08-19  8:50 [ndctl PATCH 0/2] cxl: Add cxl list image, image-from-file subcommands sunfishho12
2022-08-19  8:54 ` [ndctl PATCH 1/2] cxl: Add root port attribute to cxl list output sunfishho12
2022-09-09 22:10   ` Verma, Vishal L
2022-08-19  8:57 ` [ndctl PATCH 2/2] cxl: Add list image, image-from-file to CXL command sunfishho12
2022-09-09 22:59   ` Verma, Vishal L [this message]
2022-09-09 21:40 ` [ndctl PATCH 0/2] cxl: Add cxl list image, image-from-file subcommands Verma, Vishal L

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=093ebf126ad44920d9c7bfd1c30af5040e0c7020.camel@intel.com \
    --to=vishal.l.verma@intel.com \
    --cc=a.manzanares@samsung.com \
    --cc=dan.j.williams@intel.com \
    --cc=dave@stgolabs.net \
    --cc=linux-cxl@vger.kernel.org \
    --cc=sunfishho12@gmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox