Implement sunburst badge for QPI 35/34735/5
authorYujun Zhang <zhang.yujunz@zte.com.cn>
Sat, 13 May 2017 13:52:37 +0000 (21:52 +0800)
committerYujun Zhang <zhang.yujunz@zte.com.cn>
Tue, 16 May 2017 03:52:29 +0000 (11:52 +0800)
Change-Id: Iccdec7b0ac223a38c846f73adc6bd0e53db3723b
Signed-off-by: Yujun Zhang <zhang.yujunz@zte.com.cn>
qtip/ansible_library/plugins/action/aggregate.py
qtip/ansible_library/plugins/action/calculate.py
resources/QPI/compute.yaml
resources/ansible_roles/qtip/tasks/aggregate.yml
resources/template/qpi.html.j2 [new file with mode: 0644]
tests/data/results/expected.json
tests/unit/ansible_library/plugins/action/calculate_test.py

index f1451e0..36ea0ef 100644 (file)
@@ -42,9 +42,15 @@ class ActionModule(ActionBase):
 # aggregate QPI results
 @export_to_file
 def aggregate(hosts, basepath, src):
-    host_results = [{'host': host, 'result': json.load(open(os.path.join(basepath, host, src)))} for host in hosts]
-    score = int(mean([r['result']['score'] for r in host_results]))
+    host_results = []
+    for host in hosts:
+        host_result = json.load(open(os.path.join(basepath, host, src)))
+        host_result['name'] = host
+        host_results.append(host_result)
+    score = int(mean([r['score'] for r in host_results]))
     return {
         'score': score,
-        'host_results': host_results
+        'name': 'compute',
+        'description': 'POD Compute QPI',
+        'children': host_results
     }
index 8d5fa1f..d50222f 100644 (file)
@@ -55,18 +55,22 @@ def calc_qpi(qpi_spec, metrics):
     display.vvv("spec: {}".format(qpi_spec))
     display.vvv("metrics: {}".format(metrics))
 
-    section_results = [{'name': s['name'], 'result': calc_section(s, metrics)}
+    section_results = [calc_section(s, metrics)
                        for s in qpi_spec['sections']]
 
     # TODO(yujunz): use formula in spec
     standard_score = 2048
-    qpi_score = int(mean([r['result']['score'] for r in section_results]) * standard_score)
+    qpi_score = int(mean([r['score'] for r in section_results]) * standard_score)
 
     results = {
-        'spec': qpi_spec,
         'score': qpi_score,
-        'section_results': section_results,
-        'metrics': metrics
+        'name': qpi_spec['name'],
+        'description': qpi_spec['description'],
+        'children': section_results,
+        'details': {
+            'metrics': metrics,
+            'spec': qpi_spec
+        }
     }
 
     return results
@@ -78,13 +82,15 @@ def calc_section(section_spec, metrics):
     display.vvv("spec: {}".format(section_spec))
     display.vvv("metrics: {}".format(metrics))
 
-    metric_results = [{'name': m['name'], 'result': calc_metric(m, metrics[m['name']])}
+    metric_results = [calc_metric(m, metrics[m['name']])
                       for m in section_spec['metrics']]
     # TODO(yujunz): use formula in spec
-    section_score = mean([r['result']['score'] for r in metric_results])
+    section_score = mean([r['score'] for r in metric_results])
     return {
         'score': section_score,
-        'metric_results': metric_results
+        'name': section_spec['name'],
+        'description': section_spec.get('description', 'section'),
+        'children': metric_results
     }
 
 
@@ -95,12 +101,16 @@ def calc_metric(metric_spec, metrics):
     display.vvv("metrics: {}".format(metrics))
 
     # TODO(yujunz): use formula in spec
-    workload_results = [{'name': w['name'], 'score': calc_score(metrics[w['name']], w['baseline'])}
+    workload_results = [{'name': w['name'],
+                         'description': 'workload',
+                         'score': calc_score(metrics[w['name']], w['baseline'])}
                         for w in metric_spec['workloads']]
     metric_score = mean([r['score'] for r in workload_results])
     return {
         'score': metric_score,
-        'workload_results': workload_results
+        'name': metric_spec['name'],
+        'description': metric_spec.get('description', 'metric'),
+        'children': workload_results
     }
 
 
index e69a463..775f5c9 100644 (file)
@@ -18,7 +18,6 @@ sections: # split based on different application
         formual: geometric mean
         workloads:
           - name: rsa_sign_512
-            description: RSA signature 512 bits
             baseline: 14982.3
           - name: rsa_verify_512
             baseline: 180619.2
index 9ecdc70..904fc5d 100644 (file)
     group: compute
     basepath: "{{ qtip_results }}/current"
     src: "compute.json"
-    dest: "{{ pod_name }}-qpi.json"
+    dest: "qpi.json"
   register: pod_result
+
+- name: generating HTML report
+  template:
+    src: "{{ qtip_resources }}/template/qpi.html.j2"
+    dest: "{{ qtip_results }}/current/index.html"
diff --git a/resources/template/qpi.html.j2 b/resources/template/qpi.html.j2
new file mode 100644 (file)
index 0000000..3515676
--- /dev/null
@@ -0,0 +1,323 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<style>
+
+    circle,
+    path {
+        cursor: pointer;
+    }
+
+    circle {
+        fill: none;
+        pointer-events: all;
+    }
+
+    #tooltip {
+        background-color: white;
+        padding: 3px 5px;
+        border: 1px solid black;
+        text-align: center;
+    }
+
+    html {
+        font-family: sans-serif;
+
+    }
+</style>
+<body>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
+<script>
+
+    var margin = {top: 350, right: 480, bottom: 350, left: 480},
+        radius = Math.min(margin.top, margin.right, margin.bottom, margin.left) - 10;
+
+    function filter_min_arc_size_text(d, i) {
+        return (d.dx * d.depth * radius / 3) > 14
+    };
+
+    var hue = d3.scale.category10();
+
+    var luminance = d3.scale.sqrt()
+        .domain([0, 1e6])
+        .clamp(true)
+        .range([90, 20]);
+
+    var svg = d3.select("body").append("svg")
+        .attr("width", margin.left + margin.right)
+        .attr("height", margin.top + margin.bottom)
+        .append("g")
+        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+
+    var partition = d3.layout.partition()
+        .sort(function (a, b) {
+            return d3.ascending(a.name, b.name);
+        })
+        .size([2 * Math.PI, radius]);
+
+    var arc = d3.svg.arc()
+        .startAngle(function (d) {
+            return d.x;
+        })
+        .endAngle(function (d) {
+            return d.x + d.dx - .01 / (d.depth + .5);
+        })
+        .innerRadius(function (d) {
+            return radius / 3 * d.depth;
+        })
+        .outerRadius(function (d) {
+            return radius / 3 * (d.depth + 1) - 1;
+        });
+
+    //Tooltip description
+    var tooltip = d3.select("body")
+        .append("div")
+        .attr("id", "tooltip")
+        .style("position", "absolute")
+        .style("z-index", "10")
+        .style("opacity", 0);
+
+    function format_number(x) {
+        return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+    }
+
+
+    function format_description(d) {
+        var description = d.description;
+        return '<b>' + d.name + '</b></br>' + d.description + '<br> (' + format_number(d.value) + ')';
+    }
+
+    function computeTextRotation(d) {
+        var angle = (d.x + d.dx / 2) * 180 / Math.PI - 90
+
+        return angle;
+    }
+
+    function mouseOverArc(d) {
+        d3.select(this).attr("stroke", "black")
+
+        tooltip.html(format_description(d));
+        return tooltip.transition()
+            .duration(50)
+            .style("opacity", 0.9);
+    }
+
+    function mouseOutArc() {
+        d3.select(this).attr("stroke", "")
+        return tooltip.style("opacity", 0);
+    }
+
+    function mouseMoveArc(d) {
+        return tooltip
+            .style("top", (d3.event.pageY - 10) + "px")
+            .style("left", (d3.event.pageX + 10) + "px");
+    }
+
+    var root_ = null;
+    d3.json("qpi.json", function (error, root) {
+        if (error) return console.warn(error);
+        // Compute the initial layout on the entire tree to sum sizes.
+        // Also compute the full name and fill color for each node,
+        // and stash the children so they can be restored as we descend.
+
+        partition
+            .value(function (d) {
+                return d.score;
+            })
+            .nodes(root)
+            .forEach(function (d) {
+                d._children = d.children;
+                d.sum = d.value;
+                d.key = key(d);
+                d.fill = fill(d);
+            });
+
+        // Now redefine the value function to use the previously-computed sum.
+        partition
+            .children(function (d, depth) {
+                return depth < 2 ? d._children : null;
+            })
+            .value(function (d) {
+                return d.sum;
+            });
+
+        var center = svg.append("circle")
+            .attr("r", radius / 3)
+            .on("click", zoomOut);
+
+        center.append("title")
+            .text("zoom out");
+
+        var partitioned_data = partition.nodes(root).slice(1)
+
+        var path = svg.selectAll("path")
+            .data(partitioned_data)
+            .enter().append("path")
+            .attr("d", arc)
+            .style("fill", function (d) {
+                return d.fill;
+            })
+            .each(function (d) {
+                this._current = updateArc(d);
+            })
+            .on("click", zoomIn)
+            .on("mouseover", mouseOverArc)
+            .on("mousemove", mouseMoveArc)
+            .on("mouseout", mouseOutArc);
+
+
+        var texts = svg.selectAll("text")
+            .data(partitioned_data)
+            .enter().append("text")
+            .filter(filter_min_arc_size_text)
+            .attr("transform", function (d) {
+                return "rotate(" + computeTextRotation(d) + ")";
+            })
+            .attr("x", function (d) {
+                return radius / 3 * d.depth;
+            })
+            .attr("dx", "6") // margin
+            .attr("dy", ".35em") // vertical-align
+            .text(function (d, i) {
+                return d.name
+            })
+
+        function zoomIn(p) {
+            if (p.depth > 1) p = p.parent;
+            if (!p.children) return;
+            zoom(p, p);
+        }
+
+        function zoomOut(p) {
+            if (!p.parent) return;
+            zoom(p.parent, p);
+        }
+
+        // Zoom to the specified new root.
+        function zoom(root, p) {
+            if (document.documentElement.__transition__) return;
+
+            // Rescale outside angles to match the new layout.
+            var enterArc,
+                exitArc,
+                outsideAngle = d3.scale.linear().domain([0, 2 * Math.PI]);
+
+            function insideArc(d) {
+                return p.key > d.key
+                    ? {depth: d.depth - 1, x: 0, dx: 0} : p.key < d.key
+                        ? {depth: d.depth - 1, x: 2 * Math.PI, dx: 0}
+                        : {depth: 0, x: 0, dx: 2 * Math.PI};
+            }
+
+            function outsideArc(d) {
+                return {depth: d.depth + 1, x: outsideAngle(d.x), dx: outsideAngle(d.x + d.dx) - outsideAngle(d.x)};
+            }
+
+            center.datum(root);
+
+            // When zooming in, arcs enter from the outside and exit to the inside.
+            // Entering outside arcs start from the old layout.
+            if (root === p) enterArc = outsideArc, exitArc = insideArc, outsideAngle.range([p.x, p.x + p.dx]);
+
+            var new_data = partition.nodes(root).slice(1)
+
+            path = path.data(new_data, function (d) {
+                return d.key;
+            });
+
+            // When zooming out, arcs enter from the inside and exit to the outside.
+            // Exiting outside arcs transition to the new layout.
+            if (root !== p) enterArc = insideArc, exitArc = outsideArc, outsideAngle.range([p.x, p.x + p.dx]);
+
+            d3.transition().duration(d3.event.altKey ? 7500 : 750).each(function () {
+                path.exit().transition()
+                    .style("fill-opacity", function (d) {
+                        return d.depth === 1 + (root === p) ? 1 : 0;
+                    })
+                    .attrTween("d", function (d) {
+                        return arcTween.call(this, exitArc(d));
+                    })
+                    .remove();
+
+                path.enter().append("path")
+                    .style("fill-opacity", function (d) {
+                        return d.depth === 2 - (root === p) ? 1 : 0;
+                    })
+                    .style("fill", function (d) {
+                        return d.fill;
+                    })
+                    .on("click", zoomIn)
+                    .on("mouseover", mouseOverArc)
+                    .on("mousemove", mouseMoveArc)
+                    .on("mouseout", mouseOutArc)
+                    .each(function (d) {
+                        this._current = enterArc(d);
+                    });
+
+
+                path.transition()
+                    .style("fill-opacity", 1)
+                    .attrTween("d", function (d) {
+                        return arcTween.call(this, updateArc(d));
+                    });
+
+
+            });
+
+
+            texts = texts.data(new_data, function (d) {
+                return d.key;
+            })
+
+            texts.exit()
+                .remove()
+            texts.enter()
+                .append("text")
+
+            texts.style("opacity", 0)
+                .attr("transform", function (d) {
+                    return "rotate(" + computeTextRotation(d) + ")";
+                })
+                .attr("x", function (d) {
+                    return radius / 3 * d.depth;
+                })
+                .attr("dx", "6") // margin
+                .attr("dy", ".35em") // vertical-align
+                .filter(filter_min_arc_size_text)
+                .text(function (d, i) {
+                    return d.name
+                })
+                .transition().delay(750).style("opacity", 1)
+
+
+        }
+    });
+
+    function key(d) {
+        var k = [], p = d;
+        while (p.depth) k.push(p.name), p = p.parent;
+        return k.reverse().join(".");
+    }
+
+    function fill(d) {
+        var p = d;
+        while (p.depth > 1) p = p.parent;
+        var c = d3.lab(hue(p.name));
+        c.l = luminance(d.sum);
+        return c;
+    }
+
+    function arcTween(b) {
+        var i = d3.interpolate(this._current, b);
+        this._current = i(0);
+        return function (t) {
+            return arc(i(t));
+        };
+    }
+
+    function updateArc(d) {
+        return {depth: d.depth, x: d.x, dx: d.dx};
+    }
+
+    d3.select(self.frameElement).style("height", margin.top + margin.bottom + "px");
+
+</script>
\ No newline at end of file
index a495d99..e77200d 100644 (file)
@@ -1,7 +1,15 @@
 {
   "score": 150,
-  "host_results": [
-    {"host": "host1", "result": {"score": 100}},
-    {"host": "host2", "result": {"score": 200}}
-  ]
+  "children": [
+    {
+      "name": "host1",
+      "score": 100
+    },
+    {
+      "name": "host2",
+      "score": 200
+    }
+  ],
+  "description": "POD Compute QPI",
+  "name": "compute"
 }
index 68a03e2..31d7212 100644 (file)
@@ -45,8 +45,8 @@ def section_spec(metric_spec):
 @pytest.fixture
 def qpi_spec(section_spec):
     return {
-        "description": "QTIP Performance Index of compute",
         "name": "compute",
+        "description": "QTIP Performance Index of compute",
         "sections": [section_spec]
     }
 
@@ -54,23 +54,29 @@ def qpi_spec(section_spec):
 @pytest.fixture()
 def metric_result():
     return {'score': 1.0,
-            'workload_results': [
-                {'name': 'rsa_sign', 'score': 1.0},
-                {'name': 'rsa_verify', 'score': 1.0}]}
+            'name': 'ssl_rsa',
+            'description': 'metric',
+            'children': [{'description': 'workload', 'name': 'rsa_sign', 'score': 1.0},
+                         {'description': 'workload', 'name': 'rsa_verify', 'score': 1.0}]}
 
 
 @pytest.fixture()
 def section_result(metric_result):
     return {'score': 1.0,
-            'metric_results': [{'name': 'ssl_rsa', 'result': metric_result}]}
+            'name': 'ssl',
+            'description': 'cryptography and SSL/TLS performance',
+            'children': [metric_result]}
 
 
 @pytest.fixture()
 def qpi_result(qpi_spec, section_result, metrics):
     return {'score': 2048,
-            'spec': qpi_spec,
-            'metrics': metrics,
-            'section_results': [{'name': 'ssl', 'result': section_result}]}
+            'name': 'compute',
+            'description': 'QTIP Performance Index of compute',
+            'children': [section_result],
+            'details': {
+                'spec': qpi_spec,
+                'metrics': metrics}}
 
 
 def test_calc_metric(metric_spec, metrics, metric_result):