aboutsummaryrefslogtreecommitdiffstats
path: root/tests/perf/10ge2p1x520-ethip4-ip4scale2m-ndrchk.robot
blob: a9b8529b664ee9f98eac645ce8c4062a59e80806 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# Copyright (c) 2016 Cisco and/or its affiliates.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

*** Settings ***
| Resource | resources/libraries/robot/performance.robot
| Force Tags | 3_NODE_SINGLE_LINK_TOPO | PERFTEST | HW_ENV | NDRCHK
| ...        | NIC_Intel-X520-DA2 | ETH | IP4FWD | SCALE | FIB_2M
| Suite Setup | 3-node Performance Suite Setup with DUT's NIC model
| ... | L3 | Intel-X520-DA2
| Suite Teardown | 3-node Performance Suite Teardown
| Test Setup | Setup all DUTs before test
| Test Teardown | Run Keyword | Remove startup configuration of VPP from all DUTs
| Documentation | *Reference NDR throughput IPv4 routing verify test cases*
| ...
| ... | *[Top] Network Topologies:* TG-DUT1-DUT2-TG 3-node circular topology
| ... | with single links between nodes.
| ... | *[Enc] Packet Encapsulations:* Eth-IPv4 for IPv4 routing.
| ... | *[Cfg] DUT configuration:* DUT1 and DUT2 are configured with IPv4
| ... | routing and 2x1M static IPv4 /32 route entries. DUT1 and DUT2 tested
| ... | with 2p10GE NIC X520 Niantic by Intel.
| ... | *[Ver] TG verification:* In short performance tests, TG verifies
| ... | DUTs' throughput at ref-NDR (reference Non Drop Rate) with zero packet
| ... | loss tolerance. Ref-NDR value is periodically updated acording to
| ... | formula: ref-NDR = 0.9x NDR, where NDR is found in RFC2544 long
| ... | performance tests for the same DUT confiiguration. Test packets are
| ... | generated by TG on links to DUTs. TG traffic profile contains two L3
| ... | flow-groups (flow-group per direction, 1M flows per flow-group) with
| ... | all packets containing Ethernet header, IPv4 header with IP protocol=61
| ... | and static payload. Incrementing of IP.dst (IPv4 destination address)
| ... | field is applied to both streams.
| ... | *[Ref] Applicable standard specifications:* RFC2544.

*** Variables ***
| ${rts_per_flow}= | 1000000

*** Test Cases ***
| TC01: Verify 64B ref-NDR at 2x 3.5Mpps - DUT IPv4 Fib 2x1M - 1thread 1core 1rxq
| | [Documentation]
| | ... | [Cfg] DUT runs IPv4 routing config with 1 thread, 1 phy core, \
| | ... | 1 receive queue per NIC port. [Ver] Verify ref-NDR for 64 Byte
| | ... | frames using single trial throughput test.
| | [Tags] | 1T1C | STHREAD
| | ${framesize}= | Set Variable | 64
| | ${duration}= | Set Variable | 10
| | ${rate}= | Set Variable | 3.5mpps
| | Given Add '1' worker threads and rxqueues '1' in 3-node single-link topo
| | And   Add PCI devices to DUTs from 3-node single link topology
| | And   Add No Multi Seg to all DUTs
| | And   Add Heapsize Config to all DUTs | 3G
| | And   Apply startup configuration on all VPP DUTs
| | And   Scale IPv4 forwarding initialized in a 3-node circular topology
| | ...   | ${rts_per_flow}
| | Then  Traffic should pass with no loss | ${duration} | ${rate}
| | ...                                    | ${framesize}
| | ...                                    | 3-node-IPv4-dst-${rts_per_flow}

| TC02: Verify 1518B ref-NDR at 2x 812.74kpps - DUT IPv4 Fib 2x1M - 1thread 1core 1rxq
| | [Documentation]
| | ... | Verify ref-NDR for 1518 Byte frames using single trial throughput
| | ... | test. DUT runs IPv4 routing config with 1 thread, 1 phy core, 1
| | ... | receive queue per NIC port.
| | [Tags] | 1T1C | STHREAD
| | ${framesize}= | Set Variable | 1518
| | ${duration}= | Set Variable | 10
| | ${rate}= | Set Variable | 812743pps
| | Given Add '1' worker threads and rxqueues '1' in 3-node single-link topo
| | And   Add PCI devices to DUTs from 3-node single link topology
| | And   Add No Multi Seg to all DUTs
| | And   Add Heapsize Config to all DUTs | 3G
| | And   Apply startup configuration on all VPP DUTs
| | And   Scale IPv4 forwarding initialized in a 3-node circular topology
| | ...   | ${rts_per_flow}
| | Then  Traffic should pass with no loss | ${duration} | ${rate}
| | ...                                    | ${framesize}
| | ...                                    | 3-node-IPv4-dst-${rts_per_flow}

| TC03: Verify 9000B ref-NDR at 2x 138.58kpps - DUT IPv4 Fib 2x1M - 1thread 1core 1rxq
| | [Documentation]
| | ... | [Cfg] DUT runs IPv4 routing config with 1 thread, 1 phy core, \
| | ... | 1 receive queue per NIC port. [Ver] Verify ref-NDR for 1518 Byte
| | ... | frames using single trial throughput test.
| | [Tags] | 1T1C | STHREAD
| | ${framesize}= | Set Variable | 9000
| | ${duration}= | Set Variable | 10
| | ${rate}= | Set Variable | 138580pps
| | Given Add '1' worker threads and rxqueues '1' in 3-node single-link topo
| | And   Add PCI devices to DUTs from 3-node single link topology
| | And   Add Heapsize Config to all DUTs | 3G
| | And   Apply startup configuration on all VPP DUTs
| | And   Scale IPv4 forwarding initialized in a 3-node circular topology
| | ...   | ${rts_per_flow}
| | Then  Traffic should pass with no loss | ${duration} | ${rate}
| | ...                                    | ${framesize}
| | ...                                    | 3-node-IPv4-dst-${rts_per_flow}

| TC04: Verify 64B ref-NDR at 2x 7.5Mpps - DUT IPv4 Fib 2x1M - 2threads, 2cores, 1rxq
| | [Documentation]
| | ... | [Cfg] DUT runs IPv4 routing config with 2 threads, 2 phy cores, \
| | ... | 1 receive queue per NIC port. [Ver] Verify ref-NDR for 64 Byte
| | ... | frames using single trial throughput test.
| | [Tags] | 2T2C | MTHREAD
| | ${framesize}= | Set Variable | 64
| | ${duration}= | Set Variable | 10
| | ${rate}= | Set Variable | 7.5mpps
| | Given Add '2' worker threads and rxqueues '1' in 3-node single-link topo
| | And   Add PCI devices to DUTs from 3-node single link topology
| | And   Add No Multi Seg to all DUTs
| | And   Add Heapsize Config to all DUTs | 3G
| | And   Apply startup configuration on all VPP DUTs
| | And   Scale IPv4 forwarding initialized in a 3-node circular topology
| | ...   | ${rts_per_flow}
| | Then  Traffic should pass with no loss | ${duration} | ${rate}
| | ...                                    | ${framesize}
| | ...                                    | 3-node-IPv4-dst-${rts_per_flow}

| TC05: Verify 1518B ref-NDR at 2x 812.74kpps - DUT IPv4 Fib 2x1M - 2threads, 2cores, 1rxq
| | [Documentation]
| | ... | [Cfg] DUT runs IPv4 routing config with 2 threads, 2 phy cores, \
| | ... | 1 receive queue per NIC port. [Ver] Verify ref-NDR for 1518 Byte
| | ... | frames using single trial throughput test.
| | [Tags] | 2T2C | MTHREAD
| | ${framesize}= | Set Variable | 1518
| | ${duration}= | Set Variable | 10
| | ${rate}= | Set Variable | 812743pps
| | Given Add '2' worker threads and rxqueues '1' in 3-node single-link topo
| | And   Add PCI devices to DUTs from 3-node single link topology
| | And   Add No Multi Seg to all DUTs
| | And   Add Heapsize Config to all DUTs | 3G
| | And   Apply startup configuration on all VPP DUTs
| | And   Scale IPv4 forwarding initialized in a 3-node circular topology
| | ...   | ${rts_per_flow}
| | Then  Traffic should pass with no loss | ${duration} | ${rate}
| | ...                                    | ${framesize}
| | ...                                    | 3-node-IPv4-dst-${rts_per_flow}

| TC06: Verify 9000B ref-NDR at 2x 138.58kpps - DUT IPv4 Fib 2x1M - 2threads, 2cores, 1rxq
| | [Documentation]
| | ... | [Cfg] DUT runs IPv4 routing config with 2 threads, 2 phy cores, \
| | ... | 1 receive queue per NIC port. [Ver] Verify ref-NDR for 9000 Byte
| | ... | frames using single trial throughput test.
| | [Tags] | 2T2C | MTHREAD
| | ${framesize}= | Set Variable | 9000
| | ${duration}= | Set Variable | 10
| | ${rate}= | Set Variable | 138580pps
| | Given Add '2' worker threads and rxqueues '1' in 3-node single-link topo
| | And   Add PCI devices to DUTs from 3-node single link topology
| | And   Add Heapsize Config to all DUTs | 3G
| | And   Apply startup configuration on all VPP DUTs
| | And   Scale IPv4 forwarding initialized in a 3-node circular topology
| | ...   | ${rts_per_flow}
| | Then  Traffic should pass with no loss | ${duration} | ${rate}
| | ...                                    | ${framesize}
| | ...                                    | 3-node-IPv4-dst-${rts_per_flow}

| TC07: Verify 64B ref-NDR at 2x 10.0Mpps - DUT IPv4 Fib 2x1M - 4threads, 4cores, 2rxq
| | [Documentation]
| | ... | [Cfg] DUT runs IPv4 routing config with 4 threads, 4 phy cores, \
| | ... | 2 receive queues per NIC port. [Ver] Verify ref-NDR for 64 Byte
| | ... | frames using single trial throughput test.
| | [Tags] | 4T4C | MTHREAD
| | ${framesize}= | Set Variable | 64
| | ${duration}= | Set Variable | 10
| | ${rate}= | Set Variable | 10.0mpps
| | Given Add '4' worker threads and rxqueues '2' in 3-node single-link topo
| | And   Add PCI devices to DUTs from 3-node single link topology
| | And   Add No Multi Seg to all DUTs
| | And   Add Heapsize Config to all DUTs | 3G
| | And   Apply startup configuration on all VPP DUTs
| | And   Scale IPv4 forwarding initialized in a 3-node circular topology
| | ...   | ${rts_per_flow}
| | Then  Traffic should pass with no loss | ${duration} | ${rate}
| | ...                                    | ${framesize}
| | ...                                    | 3-node-IPv4-dst-${rts_per_flow}

| TC08: Verify 1518B ref-NDR at 2x 812.74kpps - DUT IPv4 Fib 2x1M - 4threads, 4cores, 2rxq
| | [Documentation]
| | ... | [Cfg] DUT runs IPv4 routing config with 4 threads, 4 phy cores, \
| | ... | 2 receive queues per NIC port. [Ver] Verify ref-NDR for 1518 Byte
| | ... | frames using single trial throughput test.
| | [Tags] | 4T4C | MTHREAD
| | ${framesize}= | Set Variable | 1518
| | ${duration}= | Set Variable | 10
| | ${rate}= | Set Variable | 812743pps
| | Given Add '4' worker threads and rxqueues '2' in 3-node single-link topo
| | And   Add PCI devices to DUTs from 3-node single link topology
| | And   Add No Multi Seg to all DUTs
| | And   Add Heapsize Config to all DUTs | 3G
| | And   Apply startup configuration on all VPP DUTs
| | And   Scale IPv4 forwarding initialized in a 3-node circular topology
| | ...   | ${rts_per_flow}
| | Then  Traffic should pass with no loss | ${duration} | ${rate}
| | ...                                    | ${framesize}
| | ...                                    | 3-node-IPv4-dst-${rts_per_flow}

| TC09: Verify 9000B ref-NDR at 2x 138.58kpps - DUT IPv4 Fib 2x1M - 4threads, 4cores, 2rxq
| | [Documentation]
| | ... | [Cfg] DUT runs IPv4 routing config with 4 threads, 4 phy cores, \
| | ... | 2 receive queues per NIC port. [Ver] Verify ref-NDR for 9000 Byte
| | ... | frames using single trial throughput test.
| | [Tags] | 4T4C | MTHREAD
| | ${framesize}= | Set Variable | 9000
| | ${duration}= | Set Variable | 10
| | ${rate}= | Set Variable | 138580pps
| | Given Add '4' worker threads and rxqueues '2' in 3-node single-link topo
| | And   Add PCI devices to DUTs from 3-node single link topology
| | And   Add Heapsize Config to all DUTs | 3G
| | And   Apply startup configuration on all VPP DUTs
| | And   Scale IPv4 forwarding initialized in a 3-node circular topology
| | ...   | ${rts_per_flow}
| | Then  Traffic should pass with no loss | ${duration} | ${rate}
| | ...                                    | ${framesize}
| | ...                                    | 3-node-IPv4-dst-${rts_per_flow}
id='n1444' href='#n1444'>1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669
# Copyright (c) 2020 Cisco and/or its affiliates.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Algorithms to generate plots.
"""


import re
import logging

from collections import OrderedDict
from copy import deepcopy

import hdrh.histogram
import hdrh.codec
import pandas as pd
import plotly.offline as ploff
import plotly.graph_objs as plgo

from plotly.subplots import make_subplots
from plotly.exceptions import PlotlyError

from pal_utils import mean, stdev


COLORS = [u"SkyBlue", u"Olive", u"Purple", u"Coral", u"Indigo", u"Pink",
          u"Chocolate", u"Brown", u"Magenta", u"Cyan", u"Orange", u"Black",
          u"Violet", u"Blue", u"Yellow", u"BurlyWood", u"CadetBlue", u"Crimson",
          u"DarkBlue", u"DarkCyan", u"DarkGreen", u"Green", u"GoldenRod",
          u"LightGreen", u"LightSeaGreen", u"LightSkyBlue", u"Maroon",
          u"MediumSeaGreen", u"SeaGreen", u"LightSlateGrey"]

REGEX_NIC = re.compile(r'(\d*ge\dp\d\D*\d*[a-z]*)-')


def generate_plots(spec, data):
    """Generate all plots specified in the specification file.

    :param spec: Specification read from the specification file.
    :param data: Data to process.
    :type spec: Specification
    :type data: InputData
    """

    generator = {
        u"plot_nf_reconf_box_name": plot_nf_reconf_box_name,
        u"plot_perf_box_name": plot_perf_box_name,
        u"plot_lat_err_bars_name": plot_lat_err_bars_name,
        u"plot_tsa_name": plot_tsa_name,
        u"plot_http_server_perf_box": plot_http_server_perf_box,
        u"plot_nf_heatmap": plot_nf_heatmap,
        u"plot_lat_hdrh_bar_name": plot_lat_hdrh_bar_name,
        u"plot_lat_hdrh_percentile": plot_lat_hdrh_percentile,
        u"plot_hdrh_lat_by_percentile": plot_hdrh_lat_by_percentile
    }

    logging.info(u"Generating the plots ...")
    for index, plot in enumerate(spec.plots):
        try:
            logging.info(f"  Plot nr {index + 1}: {plot.get(u'title', u'')}")
            plot[u"limits"] = spec.configuration[u"limits"]
            generator[plot[u"algorithm"]](plot, data)
            logging.info(u"  Done.")
        except NameError as err:
            logging.error(
                f"Probably algorithm {plot[u'algorithm']} is not defined: "
                f"{repr(err)}"
            )
    logging.info(u"Done.")


def plot_lat_hdrh_percentile(plot, input_data):
    """Generate the plot(s) with algorithm: plot_lat_hdrh_percentile
    specified in the specification file.

    :param plot: Plot to generate.
    :param input_data: Data to process.
    :type plot: pandas.Series
    :type input_data: InputData
    """

    # Transform the data
    plot_title = plot.get(u"title", u"")
    logging.info(
        f"    Creating the data set for the {plot.get(u'type', u'')} "
        f"{plot_title}."
    )
    data = input_data.filter_tests_by_name(
        plot, params=[u"latency", u"parent", u"tags", u"type"])
    if data is None or len(data[0][0]) == 0:
        logging.error(u"No data.")
        return

    fig = plgo.Figure()

    # Prepare the data for the plot
    directions = [u"W-E", u"E-W"]
    for color, test in enumerate(data[0][0]):
        try:
            if test[u"type"] in (u"NDRPDR",):
                if u"-pdr" in plot_title.lower():
                    ttype = u"PDR"
                elif u"-ndr" in plot_title.lower():
                    ttype = u"NDR"
                else:
                    logging.warning(f"Invalid test type: {test[u'type']}")
                    continue
                name = re.sub(REGEX_NIC, u"", test[u"parent"].
                              replace(u'-ndrpdr', u'').
                              replace(u'2n1l-', u''))
                for idx, direction in enumerate(
                        (u"direction1", u"direction2", )):
                    try:
                        hdr_lat = test[u"latency"][ttype][direction][u"hdrh"]
                        # TODO: Workaround, HDRH data must be aligned to 4
                        #       bytes, remove when not needed.
                        hdr_lat += u"=" * (len(hdr_lat) % 4)
                        xaxis = list()
                        yaxis = list()
                        hovertext = list()
                        decoded = hdrh.histogram.HdrHistogram.decode(hdr_lat)
                        for item in decoded.get_recorded_iterator():
                            percentile = item.percentile_level_iterated_to
                            if percentile != 100.0:
                                xaxis.append(100.0 / (100.0 - percentile))
                                yaxis.append(item.value_iterated_to)
                                hovertext.append(
                                    f"Test: {name}<br>"
                                    f"Direction: {directions[idx]}<br>"
                                    f"Percentile: {percentile:.5f}%<br>"
                                    f"Latency: {item.value_iterated_to}uSec"
                                )
                        fig.add_trace(
                            plgo.Scatter(
                                x=xaxis,
                                y=yaxis,
                                name=name,
                                mode=u"lines",
                                legendgroup=name,
                                showlegend=bool(idx),
                                line=dict(
                                    color=COLORS[color]
                                ),
                                hovertext=hovertext,
                                hoverinfo=u"text"
                            )
                        )
                    except hdrh.codec.HdrLengthException as err:
                        logging.warning(
                            f"No or invalid data for HDRHistogram for the test "
                            f"{name}\n{err}"
                        )
                        continue
            else:
                logging.warning(f"Invalid test type: {test[u'type']}")
                continue
        except (ValueError, KeyError) as err:
            logging.warning(repr(err))

    layout = deepcopy(plot[u"layout"])

    layout[u"title"][u"text"] = \
        f"<b>Latency:</b> {plot.get(u'graph-title', u'')}"
    fig[u"layout"].update(layout)

    # Create plot
    file_type = plot.get(u"output-file-type", u".html")
    logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
    try:
        # Export Plot
        ploff.plot(fig, show_link=False, auto_open=False,
                   filename=f"{plot[u'output-file']}{file_type}")
    except PlotlyError as err:
        logging.error(f"   Finished with error: {repr(err)}")


def plot_hdrh_lat_by_percentile(plot, input_data):
    """Generate the plot(s) with algorithm: plot_hdrh_lat_by_percentile
    specified in the specification file.

    :param plot: Plot to generate.
    :param input_data: Data to process.
    :type plot: pandas.Series
    :type input_data: InputData
    """

    # Transform the data
    logging.info(
        f"    Creating the data set for the {plot.get(u'type', u'')} "
        f"{plot.get(u'title', u'')}."
    )
    if plot.get(u"include", None):
        data = input_data.filter_tests_by_name(
            plot,
            params=[u"name", u"latency", u"parent", u"tags", u"type"]
        )[0][0]
    elif plot.get(u"filter", None):
        data = input_data.filter_data(
            plot,
            params=[u"name", u"latency", u"parent", u"tags", u"type"],
            continue_on_error=True
        )[0][0]
    else:
        job = list(plot[u"data"].keys())[0]
        build = str(plot[u"data"][job][0])
        data = input_data.tests(job, build)

    if data is None or len(data) == 0:
        logging.error(u"No data.")
        return

    desc = {
        u"LAT0": u"No-load.",
        u"PDR10": u"Low-load, 10% PDR.",
        u"PDR50": u"Mid-load, 50% PDR.",
        u"PDR90": u"High-load, 90% PDR.",
        u"PDR": u"Full-load, 100% PDR.",
        u"NDR10": u"Low-load, 10% NDR.",
        u"NDR50": u"Mid-load, 50% NDR.",
        u"NDR90": u"High-load, 90% NDR.",
        u"NDR": u"Full-load, 100% NDR."
    }

    graphs = [
        u"LAT0",
        u"PDR10",
        u"PDR50",
        u"PDR90"
    ]

    file_links = plot.get(u"output-file-links", None)
    target_links = plot.get(u"target-links", None)

    for test in data:
        try:
            if test[u"type"] not in (u"NDRPDR",):
                logging.warning(f"Invalid test type: {test[u'type']}")
                continue
            name = re.sub(REGEX_NIC, u"", test[u"parent"].
                          replace(u'-ndrpdr', u'').replace(u'2n1l-', u''))
            try:
                nic = re.search(REGEX_NIC, test[u"parent"]).group(1)
            except (IndexError, AttributeError, KeyError, ValueError):
                nic = u""
            name_link = f"{nic}-{test[u'name']}".replace(u'-ndrpdr', u'')

            logging.info(f"    Generating the graph: {name_link}")

            fig = plgo.Figure()
            layout = deepcopy(plot[u"layout"])

            for color, graph in enumerate(graphs):
                for idx, direction in enumerate((u"direction1", u"direction2")):
                    xaxis = [0.0, ]
                    yaxis = [0.0, ]
                    hovertext = [
                        f"<b>{desc[graph]}</b><br>"
                        f"Direction: {(u'W-E', u'E-W')[idx % 2]}<br>"
                        f"Percentile: 0.0%<br>"
                        f"Latency: 0.0uSec"
                    ]
                    decoded = hdrh.histogram.HdrHistogram.decode(
                        test[u"latency"][graph][direction][u"hdrh"]
                    )
                    for item in decoded.get_recorded_iterator():
                        percentile = item.percentile_level_iterated_to
                        if percentile > 99.9:
                            continue
                        xaxis.append(percentile)
                        yaxis.append(item.value_iterated_to)
                        hovertext.append(
                            f"<b>{desc[graph]}</b><br>"
                            f"Direction: {(u'W-E', u'E-W')[idx % 2]}<br>"
                            f"Percentile: {percentile:.5f}%<br>"
                            f"Latency: {item.value_iterated_to}uSec"
                        )
                    fig.add_trace(
                        plgo.Scatter(
                            x=xaxis,
                            y=yaxis,
                            name=desc[graph],
                            mode=u"lines",
                            legendgroup=desc[graph],
                            showlegend=bool(idx),
                            line=dict(
                                color=COLORS[color],
                                dash=u"solid" if idx % 2 else u"dash"
                            ),
                            hovertext=hovertext,
                            hoverinfo=u"text"
                        )
                    )

            layout[u"title"][u"text"] = f"<b>Latency:</b> {name}"
            fig.update_layout(layout)

            # Create plot
            file_name = f"{plot[u'output-file']}-{name_link}.html"
            logging.info(f"    Writing file {file_name}")

            try:
                # Export Plot
                ploff.plot(fig, show_link=False, auto_open=False,
                           filename=file_name)
                # Add link to the file:
                if file_links and target_links:
                    with open(file_links, u"a") as file_handler:
                        file_handler.write(
                            f"- `{name_link} "
                            f"<{target_links}/{file_name.split(u'/')[-1]}>`_\n"
                        )
            except FileNotFoundError as err:
                logging.error(
                    f"Not possible to write the link to the file "
                    f"{file_links}\n{err}"
                )
            except PlotlyError as err:
                logging.error(f"   Finished with error: {repr(err)}")

        except hdrh.codec.HdrLengthException as err:
            logging.warning(repr(err))
            continue

        except (ValueError, KeyError) as err:
            logging.warning(repr(err))
            continue


def plot_lat_hdrh_bar_name(plot, input_data):
    """Generate the plot(s) with algorithm: plot_lat_hdrh_bar_name
    specified in the specification file.

    :param plot: Plot to generate.
    :param input_data: Data to process.
    :type plot: pandas.Series
    :type input_data: InputData
    """

    # Transform the data
    plot_title = plot.get(u"title", u"")
    logging.info(
        f"    Creating the data set for the {plot.get(u'type', u'')} "
        f"{plot_title}."
    )
    data = input_data.filter_tests_by_name(
        plot, params=[u"latency", u"parent", u"tags", u"type"])
    if data is None or len(data[0][0]) == 0:
        logging.error(u"No data.")
        return

    # Prepare the data for the plot
    directions = [u"W-E", u"E-W"]
    tests = list()
    traces = list()
    for idx_row, test in enumerate(data[0][0]):
        try:
            if test[u"type"] in (u"NDRPDR",):
                if u"-pdr" in plot_title.lower():
                    ttype = u"PDR"
                elif u"-ndr" in plot_title.lower():
                    ttype = u"NDR"
                else:
                    logging.warning(f"Invalid test type: {test[u'type']}")
                    continue
                name = re.sub(REGEX_NIC, u"", test[u"parent"].
                              replace(u'-ndrpdr', u'').
                              replace(u'2n1l-', u''))
                histograms = list()
                for idx_col, direction in enumerate(
                        (u"direction1", u"direction2", )):
                    try:
                        hdr_lat = test[u"latency"][ttype][direction][u"hdrh"]
                        # TODO: Workaround, HDRH data must be aligned to 4
                        #       bytes, remove when not needed.
                        hdr_lat += u"=" * (len(hdr_lat) % 4)
                        xaxis = list()
                        yaxis = list()
                        hovertext = list()
                        decoded = hdrh.histogram.HdrHistogram.decode(hdr_lat)
                        total_count = decoded.get_total_count()
                        for item in decoded.get_recorded_iterator():
                            xaxis.append(item.value_iterated_to)
                            prob = float(item.count_added_in_this_iter_step) / \
                                   total_count * 100
                            yaxis.append(prob)
                            hovertext.append(
                                f"Test: {name}<br>"
                                f"Direction: {directions[idx_col]}<br>"
                                f"Latency: {item.value_iterated_to}uSec<br>"
                                f"Probability: {prob:.2f}%<br>"
                                f"Percentile: "
                                f"{item.percentile_level_iterated_to:.2f}"
                            )
                        marker_color = [COLORS[idx_row], ] * len(yaxis)
                        marker_color[xaxis.index(
                            decoded.get_value_at_percentile(50.0))] = u"red"
                        marker_color[xaxis.index(
                            decoded.get_value_at_percentile(90.0))] = u"red"
                        marker_color[xaxis.index(
                            decoded.get_value_at_percentile(95.0))] = u"red"
                        histograms.append(
                            plgo.Bar(
                                x=xaxis,
                                y=yaxis,
                                showlegend=False,
                                name=name,
                                marker={u"color": marker_color},
                                hovertext=hovertext,
                                hoverinfo=u"text"
                            )
                        )
                    except hdrh.codec.HdrLengthException as err:
                        logging.warning(
                            f"No or invalid data for HDRHistogram for the test "
                            f"{name}\n{err}"
                        )
                        continue
                if len(histograms) == 2:
                    traces.append(histograms)
                    tests.append(name)
            else:
                logging.warning(f"Invalid test type: {test[u'type']}")
                continue
        except (ValueError, KeyError) as err:
            logging.warning(repr(err))

    if not tests:
        logging.warning(f"No data for {plot_title}.")
        return

    fig = make_subplots(
        rows=len(tests),
        cols=2,
        specs=[
            [{u"type": u"bar"}, {u"type": u"bar"}] for _ in range(len(tests))
        ]
    )

    layout_axes = dict(
        gridcolor=u"rgb(220, 220, 220)",
        linecolor=u"rgb(220, 220, 220)",
        linewidth=1,
        showgrid=True,
        showline=True,
        showticklabels=True,
        tickcolor=u"rgb(220, 220, 220)",
    )

    for idx_row, test in enumerate(tests):
        for idx_col in range(2):
            fig.add_trace(
                traces[idx_row][idx_col],
                row=idx_row + 1,
                col=idx_col + 1
            )
            fig.update_xaxes(
                row=idx_row + 1,
                col=idx_col + 1,
                **layout_axes
            )
            fig.update_yaxes(
                row=idx_row + 1,
                col=idx_col + 1,
                **layout_axes
            )

    layout = deepcopy(plot[u"layout"])

    layout[u"title"][u"text"] = \
        f"<b>Latency:</b> {plot.get(u'graph-title', u'')}"
    layout[u"height"] = 250 * len(tests) + 130

    layout[u"annotations"][2][u"y"] = 1.06 - 0.008 * len(tests)
    layout[u"annotations"][3][u"y"] = 1.06 - 0.008 * len(tests)

    for idx, test in enumerate(tests):
        layout[u"annotations"].append({
            u"font": {
                u"size": 14
            },
            u"showarrow": False,
            u"text": f"<b>{test}</b>",
            u"textangle": 0,
            u"x": 0.5,
            u"xanchor": u"center",
            u"xref": u"paper",
            u"y": 1.0 - float(idx) * 1.06 / len(tests),
            u"yanchor": u"bottom",
            u"yref": u"paper"
        })

    fig[u"layout"].update(layout)

    # Create plot
    file_type = plot.get(u"output-file-type", u".html")
    logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
    try:
        # Export Plot
        ploff.plot(fig, show_link=False, auto_open=False,
                   filename=f"{plot[u'output-file']}{file_type}")
    except PlotlyError as err:
        logging.error(f"   Finished with error: {repr(err)}")


def plot_nf_reconf_box_name(plot, input_data):
    """Generate the plot(s) with algorithm: plot_nf_reconf_box_name
    specified in the specification file.

    :param plot: Plot to generate.
    :param input_data: Data to process.
    :type plot: pandas.Series
    :type input_data: InputData
    """

    # Transform the data
    logging.info(
        f"    Creating the data set for the {plot.get(u'type', u'')} "
        f"{plot.get(u'title', u'')}."
    )
    data = input_data.filter_tests_by_name(
        plot, params=[u"result", u"parent", u"tags", u"type"]
    )
    if data is None:
        logging.error(u"No data.")
        return

    # Prepare the data for the plot
    y_vals = OrderedDict()
    loss = dict()
    for job in data:
        for build in job:
            for test in build:
                if y_vals.get(test[u"parent"], None) is None:
                    y_vals[test[u"parent"]] = list()
                    loss[test[u"parent"]] = list()
                try:
                    y_vals[test[u"parent"]].append(test[u"result"][u"time"])
                    loss[test[u"parent"]].append(test[u"result"][u"loss"])
                except (KeyError, TypeError):
                    y_vals[test[u"parent"]].append(None)

    # Add None to the lists with missing data
    max_len = 0
    nr_of_samples = list()
    for val in y_vals.values():
        if len(val) > max_len:
            max_len = len(val)
        nr_of_samples.append(len(val))
    for val in y_vals.values():
        if len(val) < max_len:
            val.extend([None for _ in range(max_len - len(val))])

    # Add plot traces
    traces = list()
    df_y = pd.DataFrame(y_vals)
    df_y.head()
    for i, col in enumerate(df_y.columns):
        tst_name = re.sub(REGEX_NIC, u"",
                          col.lower().replace(u'-ndrpdr', u'').
                          replace(u'2n1l-', u''))

        traces.append(plgo.Box(
            x=[str(i + 1) + u'.'] * len(df_y[col]),
            y=[y if y else None for y in df_y[col]],
            name=(
                f"{i + 1}. "
                f"({nr_of_samples[i]:02d} "
                f"run{u's' if nr_of_samples[i] > 1 else u''}, "
                f"packets lost average: {mean(loss[col]):.1f}) "
                f"{u'-'.join(tst_name.split(u'-')[3:-2])}"
            ),
            hoverinfo=u"y+name"
        ))
    try:
        # Create plot
        layout = deepcopy(plot[u"layout"])
        layout[u"title"] = f"<b>Time Lost:</b> {layout[u'title']}"
        layout[u"yaxis"][u"title"] = u"<b>Implied Time Lost [s]</b>"
        layout[u"legend"][u"font"][u"size"] = 14
        layout[u"yaxis"].pop(u"range")
        plpl = plgo.Figure(data=traces, layout=layout)

        # Export Plot
        file_type = plot.get(u"output-file-type", u".html")
        logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
        ploff.plot(
            plpl,
            show_link=False,
            auto_open=False,
            filename=f"{plot[u'output-file']}{file_type}"
        )
    except PlotlyError as err:
        logging.error(
            f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
        )
        return


def plot_perf_box_name(plot, input_data):
    """Generate the plot(s) with algorithm: plot_perf_box_name
    specified in the specification file.

    :param plot: Plot to generate.
    :param input_data: Data to process.
    :type plot: pandas.Series
    :type input_data: InputData
    """

    # Transform the data
    logging.info(
        f"    Creating data set for the {plot.get(u'type', u'')} "
        f"{plot.get(u'title', u'')}."
    )
    data = input_data.filter_tests_by_name(
        plot, params=[u"throughput", u"result", u"parent", u"tags", u"type"])
    if data is None:
        logging.error(u"No data.")
        return

    # Prepare the data for the plot
    y_vals = OrderedDict()
    test_type = u""
    for job in data:
        for build in job:
            for test in build:
                if y_vals.get(test[u"parent"], None) is None:
                    y_vals[test[u"parent"]] = list()
                try:
                    if (test[u"type"] in (u"NDRPDR", ) and
                            u"-pdr" in plot.get(u"title", u"").lower()):
                        y_vals[test[u"parent"]].\
                            append(test[u"throughput"][u"PDR"][u"LOWER"])
                        test_type = u"NDRPDR"
                    elif (test[u"type"] in (u"NDRPDR", ) and
                          u"-ndr" in plot.get(u"title", u"").lower()):
                        y_vals[test[u"parent"]]. \
                            append(test[u"throughput"][u"NDR"][u"LOWER"])
                        test_type = u"NDRPDR"
                    elif test[u"type"] in (u"SOAK", ):
                        y_vals[test[u"parent"]].\
                            append(test[u"throughput"][u"LOWER"])
                        test_type = u"SOAK"
                    elif test[u"type"] in (u"HOSTSTACK", ):
                        if u"LDPRELOAD" in test[u"tags"]:
                            y_vals[test[u"parent"]].append(
                                float(test[u"result"][u"bits_per_second"]) / 1e3
                            )
                        elif u"VPPECHO" in test[u"tags"]:
                            y_vals[test[u"parent"]].append(
                                (float(test[u"result"][u"client"][u"tx_data"])
                                 * 8 / 1e3) /
                                ((float(test[u"result"][u"client"][u"time"]) +
                                  float(test[u"result"][u"server"][u"time"])) /
                                 2)
                            )
                        test_type = u"HOSTSTACK"
                    else:
                        continue
                except (KeyError, TypeError):
                    y_vals[test[u"parent"]].append(None)

    # Add None to the lists with missing data
    max_len = 0
    nr_of_samples = list()
    for val in y_vals.values():
        if len(val) > max_len:
            max_len = len(val)
        nr_of_samples.append(len(val))
    for val in y_vals.values():
        if len(val) < max_len:
            val.extend([None for _ in range(max_len - len(val))])

    # Add plot traces
    traces = list()
    df_y = pd.DataFrame(y_vals)
    df_y.head()
    y_max = list()
    for i, col in enumerate(df_y.columns):
        tst_name = re.sub(REGEX_NIC, u"",
                          col.lower().replace(u'-ndrpdr', u'').
                          replace(u'2n1l-', u''))
        kwargs = dict(
            x=[str(i + 1) + u'.'] * len(df_y[col]),
            y=[y / 1e6 if y else None for y in df_y[col]],
            name=(
                f"{i + 1}. "
                f"({nr_of_samples[i]:02d} "
                f"run{u's' if nr_of_samples[i] > 1 else u''}) "
                f"{tst_name}"
            ),
            hoverinfo=u"y+name"
        )
        if test_type in (u"SOAK", ):
            kwargs[u"boxpoints"] = u"all"

        traces.append(plgo.Box(**kwargs))

        try:
            val_max = max(df_y[col])
            if val_max:
                y_max.append(int(val_max / 1e6) + 2)
        except (ValueError, TypeError) as err:
            logging.error(repr(err))
            continue

    try:
        # Create plot
        layout = deepcopy(plot[u"layout"])
        if layout.get(u"title", None):
            if test_type in (u"HOSTSTACK", ):
                layout[u"title"] = f"<b>Bandwidth:</b> {layout[u'title']}"
            else:
                layout[u"title"] = f"<b>Throughput:</b> {layout[u'title']}"
        if y_max:
            layout[u"yaxis"][u"range"] = [0, max(y_max)]
        plpl = plgo.Figure(data=traces, layout=layout)

        # Export Plot
        logging.info(f"    Writing file {plot[u'output-file']}.html.")
        ploff.plot(
            plpl,
            show_link=False,
            auto_open=False,
            filename=f"{plot[u'output-file']}.html"
        )
    except PlotlyError as err:
        logging.error(
            f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
        )
        return


def plot_lat_err_bars_name(plot, input_data):
    """Generate the plot(s) with algorithm: plot_lat_err_bars_name
    specified in the specification file.

    :param plot: Plot to generate.
    :param input_data: Data to process.
    :type plot: pandas.Series
    :type input_data: InputData
    """

    # Transform the data
    plot_title = plot.get(u"title", u"")
    logging.info(
        f"    Creating data set for the {plot.get(u'type', u'')} {plot_title}."
    )
    data = input_data.filter_tests_by_name(
        plot, params=[u"latency", u"parent", u"tags", u"type"])
    if data is None:
        logging.error(u"No data.")
        return

    # Prepare the data for the plot
    y_tmp_vals = OrderedDict()
    for job in data:
        for build in job:
            for test in build:
                try:
                    logging.debug(f"test[u'latency']: {test[u'latency']}\n")
                except ValueError as err:
                    logging.warning(repr(err))
                if y_tmp_vals.get(test[u"parent"], None) is None:
                    y_tmp_vals[test[u"parent"]] = [
                        list(),  # direction1, min
                        list(),  # direction1, avg
                        list(),  # direction1, max
                        list(),  # direction2, min
                        list(),  # direction2, avg
                        list()   # direction2, max
                    ]
                try:
                    if test[u"type"] not in (u"NDRPDR", ):
                        logging.warning(f"Invalid test type: {test[u'type']}")
                        continue
                    if u"-pdr" in plot_title.lower():
                        ttype = u"PDR"
                    elif u"-ndr" in plot_title.lower():
                        ttype = u"NDR"
                    else:
                        logging.warning(
                            f"Invalid test type: {test[u'type']}"
                        )
                        continue
                    y_tmp_vals[test[u"parent"]][0].append(
                        test[u"latency"][ttype][u"direction1"][u"min"])
                    y_tmp_vals[test[u"parent"]][1].append(
                        test[u"latency"][ttype][u"direction1"][u"avg"])
                    y_tmp_vals[test[u"parent"]][2].append(
                        test[u"latency"][ttype][u"direction1"][u"max"])
                    y_tmp_vals[test[u"parent"]][3].append(
                        test[u"latency"][ttype][u"direction2"][u"min"])
                    y_tmp_vals[test[u"parent"]][4].append(
                        test[u"latency"][ttype][u"direction2"][u"avg"])
                    y_tmp_vals[test[u"parent"]][5].append(
                        test[u"latency"][ttype][u"direction2"][u"max"])
                except (KeyError, TypeError) as err:
                    logging.warning(repr(err))

    x_vals = list()
    y_vals = list()
    y_mins = list()
    y_maxs = list()
    nr_of_samples = list()
    for key, val in y_tmp_vals.items():
        name = re.sub(REGEX_NIC, u"", key.replace(u'-ndrpdr', u'').
                      replace(u'2n1l-', u''))
        x_vals.append(name)  # dir 1
        y_vals.append(mean(val[1]) if val[1] else None)
        y_mins.append(mean(val[0]) if val[0] else None)
        y_maxs.append(mean(val[2]) if val[2] else None)
        nr_of_samples.append(len(val[1]) if val[1] else 0)
        x_vals.append(name)  # dir 2
        y_vals.append(mean(val[4]) if val[4] else None)
        y_mins.append(mean(val[3]) if val[3] else None)
        y_maxs.append(mean(val[5]) if val[5] else None)
        nr_of_samples.append(len(val[3]) if val[3] else 0)

    traces = list()
    annotations = list()

    for idx, _ in enumerate(x_vals):
        if not bool(int(idx % 2)):
            direction = u"West-East"
        else:
            direction = u"East-West"
        hovertext = (
            f"No. of Runs: {nr_of_samples[idx]}<br>"
            f"Test: {x_vals[idx]}<br>"
            f"Direction: {direction}<br>"
        )
        if isinstance(y_maxs[idx], float):
            hovertext += f"Max: {y_maxs[idx]:.2f}uSec<br>"
        if isinstance(y_vals[idx], float):
            hovertext += f"Mean: {y_vals[idx]:.2f}uSec<br>"
        if isinstance(y_mins[idx], float):
            hovertext += f"Min: {y_mins[idx]:.2f}uSec"

        if isinstance(y_maxs[idx], float) and isinstance(y_vals[idx], float):
            array = [y_maxs[idx] - y_vals[idx], ]
        else:
            array = [None, ]
        if isinstance(y_mins[idx], float) and isinstance(y_vals[idx], float):
            arrayminus = [y_vals[idx] - y_mins[idx], ]
        else:
            arrayminus = [None, ]
        traces.append(plgo.Scatter(
            x=[idx, ],
            y=[y_vals[idx], ],
            name=x_vals[idx],
            legendgroup=x_vals[idx],
            showlegend=bool(int(idx % 2)),
            mode=u"markers",
            error_y=dict(
                type=u"data",
                symmetric=False,
                array=array,
                arrayminus=arrayminus,
                color=COLORS[int(idx / 2)]
            ),
            marker=dict(
                size=10,
                color=COLORS[int(idx / 2)],
            ),
            text=hovertext,
            hoverinfo=u"text",
        ))
        annotations.append(dict(
            x=idx,
            y=0,
            xref=u"x",
            yref=u"y",
            xanchor=u"center",
            yanchor=u"top",
            text=u"E-W" if bool(int(idx % 2)) else u"W-E",
            font=dict(
                size=16,
            ),
            align=u"center",
            showarrow=False
        ))

    try:
        # Create plot
        file_type = plot.get(u"output-file-type", u".html")
        logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
        layout = deepcopy(plot[u"layout"])
        if layout.get(u"title", None):
            layout[u"title"] = f"<b>Latency:</b> {layout[u'title']}"
        layout[u"annotations"] = annotations
        plpl = plgo.Figure(data=traces, layout=layout)

        # Export Plot
        ploff.plot(
            plpl,
            show_link=False, auto_open=False,
            filename=f"{plot[u'output-file']}{file_type}"
        )
    except PlotlyError as err:
        logging.error(
            f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
        )
        return


def plot_tsa_name(plot, input_data):
    """Generate the plot(s) with algorithm:
    plot_tsa_name
    specified in the specification file.

    :param plot: Plot to generate.
    :param input_data: Data to process.
    :type plot: pandas.Series
    :type input_data: InputData
    """

    # Transform the data
    plot_title = plot.get(u"title", u"")
    logging.info(
        f"    Creating data set for the {plot.get(u'type', u'')} {plot_title}."
    )
    data = input_data.filter_tests_by_name(
        plot, params=[u"throughput", u"parent", u"tags", u"type"])
    if data is None:
        logging.error(u"No data.")
        return

    y_vals = OrderedDict()
    for job in data:
        for build in job:
            for test in build:
                if y_vals.get(test[u"parent"], None) is None:
                    y_vals[test[u"parent"]] = {
                        u"1": list(),
                        u"2": list(),
                        u"4": list()
                    }
                try:
                    if test[u"type"] not in (u"NDRPDR",):
                        continue

                    if u"-pdr" in plot_title.lower():
                        ttype = u"PDR"
                    elif u"-ndr" in plot_title.lower():
                        ttype = u"NDR"
                    else:
                        continue

                    if u"1C" in test[u"tags"]:
                        y_vals[test[u"parent"]][u"1"]. \
                            append(test[u"throughput"][ttype][u"LOWER"])
                    elif u"2C" in test[u"tags"]:
                        y_vals[test[u"parent"]][u"2"]. \
                            append(test[u"throughput"][ttype][u"LOWER"])
                    elif u"4C" in test[u"tags"]:
                        y_vals[test[u"parent"]][u"4"]. \
                            append(test[u"throughput"][ttype][u"LOWER"])
                except (KeyError, TypeError):
                    pass

    if not y_vals:
        logging.warning(f"No data for the plot {plot.get(u'title', u'')}")
        return

    y_1c_max = dict()
    for test_name, test_vals in y_vals.items():
        for key, test_val in test_vals.items():
            if test_val:
                avg_val = sum(test_val) / len(test_val)
                y_vals[test_name][key] = [avg_val, len(test_val)]
                ideal = avg_val / (int(key) * 1e6)
                if test_name not in y_1c_max or ideal > y_1c_max[test_name]:
                    y_1c_max[test_name] = ideal

    vals = OrderedDict()
    y_max = list()
    nic_limit = 0
    lnk_limit = 0
    pci_limit = plot[u"limits"][u"pci"][u"pci-g3-x8"]
    for test_name, test_vals in y_vals.items():
        try:
            if test_vals[u"1"][1]:
                name = re.sub(
                    REGEX_NIC,
                    u"",
                    test_name.replace(u'-ndrpdr', u'').replace(u'2n1l-', u'')
                )
                vals[name] = OrderedDict()
                y_val_1 = test_vals[u"1"][0] / 1e6
                y_val_2 = test_vals[u"2"][0] / 1e6 if test_vals[u"2"][0] \
                    else None
                y_val_4 = test_vals[u"4"][0] / 1e6 if test_vals[u"4"][0] \
                    else None

                vals[name][u"val"] = [y_val_1, y_val_2, y_val_4]
                vals[name][u"rel"] = [1.0, None, None]
                vals[name][u"ideal"] = [
                    y_1c_max[test_name],
                    y_1c_max[test_name] * 2,
                    y_1c_max[test_name] * 4
                ]
                vals[name][u"diff"] = [
                    (y_val_1 - y_1c_max[test_name]) * 100 / y_val_1, None, None
                ]
                vals[name][u"count"] = [
                    test_vals[u"1"][1],
                    test_vals[u"2"][1],
                    test_vals[u"4"][1]
                ]

                try:
                    val_max = max(vals[name][u"val"])
                except ValueError as err:
                    logging.error(repr(err))
                    continue
                if val_max:
                    y_max.append(val_max)

                if y_val_2:
                    vals[name][u"rel"][1] = round(y_val_2 / y_val_1, 2)
                    vals[name][u"diff"][1] = \
                        (y_val_2 - vals[name][u"ideal"][1]) * 100 / y_val_2
                if y_val_4:
                    vals[name][u"rel"][2] = round(y_val_4 / y_val_1, 2)
                    vals[name][u"diff"][2] = \
                        (y_val_4 - vals[name][u"ideal"][2]) * 100 / y_val_4
        except IndexError as err:
            logging.warning(f"No data for {test_name}")
            logging.warning(repr(err))

        # Limits:
        if u"x520" in test_name:
            limit = plot[u"limits"][u"nic"][u"x520"]
        elif u"x710" in test_name:
            limit = plot[u"limits"][u"nic"][u"x710"]
        elif u"xxv710" in test_name:
            limit = plot[u"limits"][u"nic"][u"xxv710"]
        elif u"xl710" in test_name:
            limit = plot[u"limits"][u"nic"][u"xl710"]
        elif u"x553" in test_name:
            limit = plot[u"limits"][u"nic"][u"x553"]
        elif u"cx556a" in test_name:
            limit = plot[u"limits"][u"nic"][u"cx556a"]
        else:
            limit = 0
        if limit > nic_limit:
            nic_limit = limit

        mul = 2 if u"ge2p" in test_name else 1
        if u"10ge" in test_name:
            limit = plot[u"limits"][u"link"][u"10ge"] * mul
        elif u"25ge" in test_name:
            limit = plot[u"limits"][u"link"][u"25ge"] * mul
        elif u"40ge" in test_name:
            limit = plot[u"limits"][u"link"][u"40ge"] * mul
        elif u"100ge" in test_name:
            limit = plot[u"limits"][u"link"][u"100ge"] * mul
        else:
            limit = 0
        if limit > lnk_limit:
            lnk_limit = limit

    traces = list()
    annotations = list()
    x_vals = [1, 2, 4]

    # Limits:
    try:
        threshold = 1.1 * max(y_max)  # 10%
    except ValueError as err:
        logging.error(err)
        return
    nic_limit /= 1e6
    traces.append(plgo.Scatter(
        x=x_vals,
        y=[nic_limit, ] * len(x_vals),
        name=f"NIC: {nic_limit:.2f}Mpps",
        showlegend=False,
        mode=u"lines",
        line=dict(
            dash=u"dot",
            color=COLORS[-1],
            width=1),
        hoverinfo=u"none"
    ))
    annotations.append(dict(
        x=1,
        y=nic_limit,
        xref=u"x",
        yref=u"y",
        xanchor=u"left",
        yanchor=u"bottom",
        text=f"NIC: {nic_limit:.2f}Mpps",
        font=dict(
            size=14,
            color=COLORS[-1],
        ),
        align=u"left",
        showarrow=False
    ))
    y_max.append(nic_limit)

    lnk_limit /= 1e6
    if lnk_limit < threshold:
        traces.append(plgo.Scatter(
            x=x_vals,
            y=[lnk_limit, ] * len(x_vals),
            name=f"Link: {lnk_limit:.2f}Mpps",
            showlegend=False,
            mode=u"lines",
            line=dict(
                dash=u"dot",
                color=COLORS[-2],
                width=1),
            hoverinfo=u"none"
        ))
        annotations.append(dict(
            x=1,
            y=lnk_limit,
            xref=u"x",
            yref=u"y",
            xanchor=u"left",
            yanchor=u"bottom",
            text=f"Link: {lnk_limit:.2f}Mpps",
            font=dict(
                size=14,
                color=COLORS[-2],
            ),
            align=u"left",
            showarrow=False
        ))
        y_max.append(lnk_limit)

    pci_limit /= 1e6
    if (pci_limit < threshold and
            (pci_limit < lnk_limit * 0.95 or lnk_limit > lnk_limit * 1.05)):
        traces.append(plgo.Scatter(
            x=x_vals,
            y=[pci_limit, ] * len(x_vals),
            name=f"PCIe: {pci_limit:.2f}Mpps",
            showlegend=False,
            mode=u"lines",
            line=dict(
                dash=u"dot",
                color=COLORS[-3],
                width=1),
            hoverinfo=u"none"
        ))
        annotations.append(dict(
            x=1,
            y=pci_limit,
            xref=u"x",
            yref=u"y",
            xanchor=u"left",
            yanchor=u"bottom",
            text=f"PCIe: {pci_limit:.2f}Mpps",
            font=dict(
                size=14,
                color=COLORS[-3],
            ),
            align=u"left",
            showarrow=False
        ))
        y_max.append(pci_limit)

    # Perfect and measured:
    cidx = 0
    for name, val in vals.items():
        hovertext = list()
        try:
            for idx in range(len(val[u"val"])):
                htext = ""
                if isinstance(val[u"val"][idx], float):
                    htext += (
                        f"No. of Runs: {val[u'count'][idx]}<br>"
                        f"Mean: {val[u'val'][idx]:.2f}Mpps<br>"
                    )
                if isinstance(val[u"diff"][idx], float):
                    htext += f"Diff: {round(val[u'diff'][idx]):.0f}%<br>"
                if isinstance(val[u"rel"][idx], float):
                    htext += f"Speedup: {val[u'rel'][idx]:.2f}"
                hovertext.append(htext)
            traces.append(
                plgo.Scatter(
                    x=x_vals,
                    y=val[u"val"],
                    name=name,
                    legendgroup=name,
                    mode=u"lines+markers",
                    line=dict(
                        color=COLORS[cidx],
                        width=2),
                    marker=dict(
                        symbol=u"circle",
                        size=10
                    ),
                    text=hovertext,
                    hoverinfo=u"text+name"
                )
            )
            traces.append(
                plgo.Scatter(
                    x=x_vals,
                    y=val[u"ideal"],
                    name=f"{name} perfect",
                    legendgroup=name,
                    showlegend=False,
                    mode=u"lines",
                    line=dict(
                        color=COLORS[cidx],
                        width=2,
                        dash=u"dash"),
                    text=[f"Perfect: {y:.2f}Mpps" for y in val[u"ideal"]],
                    hoverinfo=u"text"
                )
            )
            cidx += 1
        except (IndexError, ValueError, KeyError) as err:
            logging.warning(f"No data for {name}\n{repr(err)}")

    try:
        # Create plot
        file_type = plot.get(u"output-file-type", u".html")
        logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
        layout = deepcopy(plot[u"layout"])
        if layout.get(u"title", None):
            layout[u"title"] = f"<b>Speedup Multi-core:</b> {layout[u'title']}"
        layout[u"yaxis"][u"range"] = [0, int(max(y_max) * 1.1)]
        layout[u"annotations"].extend(annotations)
        plpl = plgo.Figure(data=traces, layout=layout)

        # Export Plot
        ploff.plot(
            plpl,
            show_link=False,
            auto_open=False,
            filename=f"{plot[u'output-file']}{file_type}"
        )
    except PlotlyError as err:
        logging.error(
            f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
        )
        return


def plot_http_server_perf_box(plot, input_data):
    """Generate the plot(s) with algorithm: plot_http_server_perf_box
    specified in the specification file.

    :param plot: Plot to generate.
    :param input_data: Data to process.
    :type plot: pandas.Series
    :type input_data: InputData
    """

    # Transform the data
    logging.info(
        f"    Creating the data set for the {plot.get(u'type', u'')} "
        f"{plot.get(u'title', u'')}."
    )
    data = input_data.filter_data(plot)
    if data is None:
        logging.error(u"No data.")
        return

    # Prepare the data for the plot
    y_vals = dict()
    for job in data:
        for build in job:
            for test in build:
                if y_vals.get(test[u"name"], None) is None:
                    y_vals[test[u"name"]] = list()
                try:
                    y_vals[test[u"name"]].append(test[u"result"])
                except (KeyError, TypeError):
                    y_vals[test[u"name"]].append(None)

    # Add None to the lists with missing data
    max_len = 0
    nr_of_samples = list()
    for val in y_vals.values():
        if len(val) > max_len:
            max_len = len(val)
        nr_of_samples.append(len(val))
    for val in y_vals.values():
        if len(val) < max_len:
            val.extend([None for _ in range(max_len - len(val))])

    # Add plot traces
    traces = list()
    df_y = pd.DataFrame(y_vals)
    df_y.head()
    for i, col in enumerate(df_y.columns):
        name = \
            f"{i + 1}. " \
            f"({nr_of_samples[i]:02d} " \
            f"run{u's' if nr_of_samples[i] > 1 else u''}) " \
            f"{col.lower().replace(u'-ndrpdr', u'')}"
        if len(name) > 50:
            name_lst = name.split(u'-')
            name = u""
            split_name = True
            for segment in name_lst:
                if (len(name) + len(segment) + 1) > 50 and split_name:
                    name += u"<br>    "
                    split_name = False
                name += segment + u'-'
            name = name[:-1]

        traces.append(plgo.Box(x=[str(i + 1) + u'.'] * len(df_y[col]),
                               y=df_y[col],
                               name=name,
                               **plot[u"traces"]))
    try:
        # Create plot
        plpl = plgo.Figure(data=traces, layout=plot[u"layout"])

        # Export Plot
        logging.info(
            f"    Writing file {plot[u'output-file']}"
            f"{plot[u'output-file-type']}."
        )
        ploff.plot(
            plpl,
            show_link=False,
            auto_open=False,
            filename=f"{plot[u'output-file']}{plot[u'output-file-type']}"
        )
    except PlotlyError as err:
        logging.error(
            f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
        )
        return


def plot_nf_heatmap(plot, input_data):
    """Generate the plot(s) with algorithm: plot_nf_heatmap
    specified in the specification file.

    :param plot: Plot to generate.
    :param input_data: Data to process.
    :type plot: pandas.Series
    :type input_data: InputData
    """

    regex_cn = re.compile(r'^(\d*)R(\d*)C$')
    regex_test_name = re.compile(r'^.*-(\d+ch|\d+pl)-'
                                 r'(\d+mif|\d+vh)-'
                                 r'(\d+vm\d+t|\d+dcr\d+t|\d+dcr\d+c).*$')
    vals = dict()

    # Transform the data
    logging.info(
        f"    Creating the data set for the {plot.get(u'type', u'')} "
        f"{plot.get(u'title', u'')}."
    )
    data = input_data.filter_data(plot, continue_on_error=True)
    if data is None or data.empty:
        logging.error(u"No data.")
        return

    for job in data:
        for build in job:
            for test in build:
                for tag in test[u"tags"]:
                    groups = re.search(regex_cn, tag)
                    if groups:
                        chain = str(groups.group(1))
                        node = str(groups.group(2))
                        break
                else:
                    continue
                groups = re.search(regex_test_name, test[u"name"])
                if groups and len(groups.groups()) == 3:
                    hover_name = (
                        f"{str(groups.group(1))}-"
                        f"{str(groups.group(2))}-"
                        f"{str(groups.group(3))}"
                    )
                else:
                    hover_name = u""
                if vals.get(chain, None) is None:
                    vals[chain] = dict()
                if vals[chain].get(node, None) is None:
                    vals[chain][node] = dict(
                        name=hover_name,
                        vals=list(),
                        nr=None,
                        mean=None,
                        stdev=None
                    )
                try:
                    if plot[u"include-tests"] == u"MRR":
                        result = test[u"result"][u"receive-rate"]
                    elif plot[u"include-tests"] == u"PDR":
                        result = test[u"throughput"][u"PDR"][u"LOWER"]
                    elif plot[u"include-tests"] == u"NDR":
                        result = test[u"throughput"][u"NDR"][u"LOWER"]
                    else:
                        result = None
                except TypeError:
                    result = None

                if result:
                    vals[chain][node][u"vals"].append(result)

    if not vals:
        logging.error(u"No data.")
        return

    txt_chains = list()
    txt_nodes = list()
    for key_c in vals:
        txt_chains.append(key_c)
        for key_n in vals[key_c].keys():
            txt_nodes.append(key_n)
            if vals[key_c][key_n][u"vals"]:
                vals[key_c][key_n][u"nr"] = len(vals[key_c][key_n][u"vals"])
                vals[key_c][key_n][u"mean"] = \
                    round(mean(vals[key_c][key_n][u"vals"]) / 1000000, 1)
                vals[key_c][key_n][u"stdev"] = \
                    round(stdev(vals[key_c][key_n][u"vals"]) / 1000000, 1)
    txt_nodes = list(set(txt_nodes))

    def sort_by_int(value):
        """Makes possible to sort a list of strings which represent integers.

        :param value: Integer as a string.
        :type value: str
        :returns: Integer representation of input parameter 'value'.
        :rtype: int
        """
        return int(value)

    txt_chains = sorted(txt_chains, key=sort_by_int)
    txt_nodes = sorted(txt_nodes, key=sort_by_int)

    chains = [i + 1 for i in range(len(txt_chains))]
    nodes = [i + 1 for i in range(len(txt_nodes))]

    data = [list() for _ in range(len(chains))]
    for chain in chains:
        for node in nodes:
            try:
                val = vals[txt_chains[chain - 1]][txt_nodes[node - 1]][u"mean"]
            except (KeyError, IndexError):
                val = None
            data[chain - 1].append(val)

    # Color scales:
    my_green = [[0.0, u"rgb(235, 249, 242)"],
                [1.0, u"rgb(45, 134, 89)"]]

    my_blue = [[0.0, u"rgb(236, 242, 248)"],
               [1.0, u"rgb(57, 115, 172)"]]

    my_grey = [[0.0, u"rgb(230, 230, 230)"],
               [1.0, u"rgb(102, 102, 102)"]]

    hovertext = list()
    annotations = list()

    text = (u"Test: {name}<br>"
            u"Runs: {nr}<br>"
            u"Thput: {val}<br>"
            u"StDev: {stdev}")

    for chain, _ in enumerate(txt_chains):
        hover_line = list()
        for node, _ in enumerate(txt_nodes):
            if data[chain][node] is not None:
                annotations.append(
                    dict(
                        x=node+1,
                        y=chain+1,
                        xref=u"x",
                        yref=u"y",
                        xanchor=u"center",
                        yanchor=u"middle",
                        text=str(data[chain][node]),
                        font=dict(
                            size=14,
                        ),
                        align=u"center",
                        showarrow=False
                    )
                )
                hover_line.append(text.format(
                    name=vals[txt_chains[chain]][txt_nodes[node]][u"name"],
                    nr=vals[txt_chains[chain]][txt_nodes[node]][u"nr"],
                    val=data[chain][node],
                    stdev=vals[txt_chains[chain]][txt_nodes[node]][u"stdev"]))
        hovertext.append(hover_line)

    traces = [
        plgo.Heatmap(
            x=nodes,
            y=chains,
            z=data,
            colorbar=dict(
                title=plot.get(u"z-axis", u""),
                titleside=u"right",
                titlefont=dict(
                    size=16
                ),
                tickfont=dict(
                    size=16,
                ),
                tickformat=u".1f",
                yanchor=u"bottom",
                y=-0.02,
                len=0.925,
            ),
            showscale=True,
            colorscale=my_green,
            text=hovertext,
            hoverinfo=u"text"
        )
    ]

    for idx, item in enumerate(txt_nodes):
        # X-axis, numbers:
        annotations.append(
            dict(
                x=idx+1,
                y=0.05,
                xref=u"x",
                yref=u"y",
                xanchor=u"center",
                yanchor=u"top",
                text=item,
                font=dict(
                    size=16,
                ),
                align=u"center",
                showarrow=False
            )
        )
    for idx, item in enumerate(txt_chains):
        # Y-axis, numbers:
        annotations.append(
            dict(
                x=0.35,
                y=idx+1,
                xref=u"x",
                yref=u"y",
                xanchor=u"right",
                yanchor=u"middle",
                text=item,
                font=dict(
                    size=16,
                ),
                align=u"center",
                showarrow=False
            )
        )
    # X-axis, title:
    annotations.append(
        dict(
            x=0.55,
            y=-0.15,
            xref=u"paper",
            yref=u"y",
            xanchor=u"center",
            yanchor=u"bottom",
            text=plot.get(u"x-axis", u""),
            font=dict(
                size=16,
            ),
            align=u"center",
            showarrow=False
        )
    )
    # Y-axis, title:
    annotations.append(
        dict(
            x=-0.1,
            y=0.5,
            xref=u"x",
            yref=u"paper",
            xanchor=u"center",
            yanchor=u"middle",
            text=plot.get(u"y-axis", u""),
            font=dict(
                size=16,
            ),
            align=u"center",
            textangle=270,
            showarrow=False
        )
    )
    updatemenus = list([
        dict(
            x=1.0,
            y=0.0,
            xanchor=u"right",
            yanchor=u"bottom",
            direction=u"up",
            buttons=list([
                dict(
                    args=[
                        {
                            u"colorscale": [my_green, ],
                            u"reversescale": False
                        }
                    ],
                    label=u"Green",
                    method=u"update"
                ),
                dict(
                    args=[
                        {
                            u"colorscale": [my_blue, ],
                            u"reversescale": False
                        }
                    ],
                    label=u"Blue",
                    method=u"update"
                ),
                dict(
                    args=[
                        {
                            u"colorscale": [my_grey, ],
                            u"reversescale": False
                        }
                    ],
                    label=u"Grey",
                    method=u"update"
                )
            ])
        )
    ])

    try:
        layout = deepcopy(plot[u"layout"])
    except KeyError as err:
        logging.error(f"Finished with error: No layout defined\n{repr(err)}")
        return

    layout[u"annotations"] = annotations
    layout[u'updatemenus'] = updatemenus

    try:
        # Create plot
        plpl = plgo.Figure(data=traces, layout=layout)

        # Export Plot
        logging.info(f"    Writing file {plot[u'output-file']}.html")
        ploff.plot(
            plpl,
            show_link=False,
            auto_open=False,
            filename=f"{plot[u'output-file']}.html"
        )
    except PlotlyError as err:
        logging.error(
            f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
        )
        return