From patchwork Fri Aug 19 08:57:01 2022 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Matthew Ho X-Patchwork-Id: 12948566 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 1C9BFC25B0E for ; Fri, 19 Aug 2022 08:57:13 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1346495AbiHSI5M (ORCPT ); Fri, 19 Aug 2022 04:57:12 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:49776 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1345863AbiHSI5K (ORCPT ); Fri, 19 Aug 2022 04:57:10 -0400 Received: from mail-pf1-x431.google.com (mail-pf1-x431.google.com [IPv6:2607:f8b0:4864:20::431]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 89099EF9E0 for ; Fri, 19 Aug 2022 01:57:09 -0700 (PDT) Received: by mail-pf1-x431.google.com with SMTP id o7so3764807pfb.9 for ; Fri, 19 Aug 2022 01:57:09 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc; bh=TOSvb3CHSn2kFiUXMwbrDCQ/WOdUmMloOXyNnlQ7WZ4=; b=ZCf7LSGuMnyGJrl4dEa78Ur3cSGfq+pY7J4eOSvbsPlkXC5z3i4c+e1z8jwsnrmE6b Ev74RdR8+wq06LgVLRZ7v0kkG7E+8qBUJhCtU9g//qkvpTI/HS8o6LQYsxDNfkRFA4rX b2esCfnf3RsDSYNaCVuCeyyyTO2FSVfQRQ6jCB/aEIbdrhj14E7l/baqvnU71jcYYoaY 4ygceG5CtLhoyKNzyTaW3ogpQp6GBvLlfYqFActi80Xhd+hAOfpyyonJglfDhl6Ehfr9 MGyXY9B3v/FRzuQ+0w7RVVONYZnTZwdIt8F5ZOZsozg9QQ21/LUi8WJN85ab+HI77cfr rH+w== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc; bh=TOSvb3CHSn2kFiUXMwbrDCQ/WOdUmMloOXyNnlQ7WZ4=; b=1O/XNAk6qmpu0vtDyVWNbQ7HMe6OfyPa6cBjePOgqZaRhR1LSFz9ZNBrf6XSoMt1bS uPWGa6fJtxKRbrjG+7arNlysoBzZTw5utf2ZNdqQOro6Ppo+C1JPuzTqb82nes+9Z+DM 76FwxvdK+9kNUBe3JwZ0zQM6T2lA5qNoxrMllzEoTZcrTHv8rGg43vgOGfhjQYNwCEw0 7zUmPkvkBT9l/UtOnudlR1+EZDIOEWK0zWBinVaA8sQUTIgjkOAGjok0t+ilQdgTfA4A NOLNVo3Zgo455Sc9JrpsWnb/2LztxiGpnY5lzHuLDO/Kb4H5uEeVXNE9a9SEeDqMs3v1 mbmQ== X-Gm-Message-State: ACgBeo1VBwJzkIdFxEzg3YBx5p5Pm3joew4mCMDA0GsOM1pDlWN5jnZo rkc19Wr9UOb2ijoakNnfO5P2S9qASJs= X-Google-Smtp-Source: AA6agR7CQO2x11K34x+mWEI9rB17qzgzdvwJ8HGNdybBSH3zg0nDhLcbs/BtWmOUzVUumr/bku2zTQ== X-Received: by 2002:a63:4e56:0:b0:428:ee87:3791 with SMTP id o22-20020a634e56000000b00428ee873791mr5425882pgl.11.1660899428635; Fri, 19 Aug 2022 01:57:08 -0700 (PDT) Received: from localhost.localdomain (c-107-3-179-31.hsd1.ca.comcast.net. [107.3.179.31]) by smtp.gmail.com with ESMTPSA id i123-20020a626d81000000b0052e7debb8desm2952392pfc.121.2022.08.19.01.57.07 (version=TLS1_3 cipher=TLS_CHACHA20_POLY1305_SHA256 bits=256/256); Fri, 19 Aug 2022 01:57:08 -0700 (PDT) From: sunfishho12@gmail.com To: linux-cxl@vger.kernel.org Cc: dan.j.williams@intel.com, vishal.l.verma@intel.com, dave@stgolabs.net, a.manzanares@samsung.com, Matthew Ho Subject: [ndctl PATCH 2/2] cxl: Add list image, image-from-file to CXL command Date: Fri, 19 Aug 2022 01:57:01 -0700 Message-Id: X-Mailer: git-send-email 2.32.0 (Apple Git-132) In-Reply-To: References: MIME-Version: 1.0 Precedence: bulk List-ID: X-Mailing-List: linux-cxl@vger.kernel.org From: Matthew Ho 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 --- 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:: + 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:: + 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 #include #include +#include #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"; + 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); +} + 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); 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 #include +#include +#include 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", ¶m.partition, "include memory device partition information"), + OPT_BOOLEAN(0, "image", ¶m.image, + "display CXL topology"), + OPT_STRING(0, "image-from-file", ¶m.image_input_file, + "input file path for image", + "path to file containing a json array describing the topology"), + OPT_STRING('o', "output-file", ¶m.output_file, "output file path", + "path to file to output diagram to"), OPT_INCR('v', "verbose", ¶m.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)