From patchwork Thu May 15 21:23:15 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: denisova-ok X-Patchwork-Id: 63080 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 aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id DEF22C3ABD8 for ; Fri, 16 May 2025 04:32:42 +0000 (UTC) Received: from forward103a.mail.yandex.net (forward103a.mail.yandex.net [178.154.239.86]) by mx.groups.io with SMTP id smtpd.web11.2550.1747344209769044250 for ; Thu, 15 May 2025 14:23:30 -0700 Authentication-Results: mx.groups.io; dkim=fail reason="dkim: body hash did not verify" header.i=@yandex.ru header.s=mail header.b=bdf0moM7; spf=pass (domain: yandex.ru, ip: 178.154.239.86, mailfrom: denisova.olga.k@yandex.ru) Received: from mail-nwsmtp-smtp-production-main-81.vla.yp-c.yandex.net (mail-nwsmtp-smtp-production-main-81.vla.yp-c.yandex.net [IPv6:2a02:6b8:c1d:4795:0:640:c576:0]) by forward103a.mail.yandex.net (Yandex) with ESMTPS id 8E6AF60916 for ; Fri, 16 May 2025 00:23:26 +0300 (MSK) Received: by mail-nwsmtp-smtp-production-main-81.vla.yp-c.yandex.net (smtp/Yandex) with ESMTPSA id PNQdiu2Ln8c0-ZcIfJ9Uu; Fri, 16 May 2025 00:23:26 +0300 X-Yandex-Fwd: 1 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=yandex.ru; s=mail; t=1747344206; bh=J+L7NXqFSaSrrUYUH1oqz8jT31EMQ6rZqF3cWvslRfI=; h=Message-Id:Date:Cc:Subject:To:From; b=bdf0moM7K/62wCpYKcr42aW0Cqn2+Z35bwoQ4vG1ru9e+RgPiIVQoOGbQv2heoxSH qp//fxcm/1/g9FEdri4pYeU4XEE7UuvlK5YMhp02zbAumutVT+flQp925IzzNzX8pI zYv3EnI6VkWr5Eq8aeRoSALs4xu5q9GHzv4bDsDQ= Authentication-Results: mail-nwsmtp-smtp-production-main-81.vla.yp-c.yandex.net; dkim=pass header.i=@yandex.ru From: denisova-ok To: openembedded-core@lists.openembedded.org Cc: Olga Denisova Subject: [OE-core][PATCH v2] pybootchartgui: visualize /proc/net/dev network stats in graphs Date: Fri, 16 May 2025 00:23:15 +0300 Message-Id: <20250515212315.6139-1-denisova.olga.k@yandex.ru> X-Mailer: git-send-email 2.34.1 MIME-Version: 1.0 List-Id: X-Webhook-Received: from li982-79.members.linode.com [45.33.32.79] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Fri, 16 May 2025 04:32:42 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/216720 From: Olga Denisova This patch adds support for parsing and visualizing network interface statistics from /proc/net/dev in pybootchartgui. It introduces a new NetSample class to hold per-interface metrics, including received/transmitted bytes and their deltas over time. The data is drawn using line and box charts in draw.py and helps to monitor network usage during the build process for each interface individually. Sample output: https://drive.google.com/file/d/1fX-OO6PSw6iFY5dlCttITKNpuR5p1nyd/view?usp=sharing The link provided contains an example image of bootchart.png, generated using the script `/scripts/pybootchartgui/pybootchartgui.py`. Additional graphs showing the usage statistics of each network interface are added to the image. These graphs are placed at the top of the image — directly below the CPU load and disk throughput graphs. The source data for the graphs is read from the file `reduced_proc_net.log`, which is generated by a separate patch. The graphs display lines showing the total number of bytes transmitted and received by each network interface, as well as the deltas between the current and previous measurements of these metrics. v2: - Added sample output into the commit message --- pybootchartgui/pybootchartgui/draw.py | 48 ++++++++++++++++++++++++ pybootchartgui/pybootchartgui/parsing.py | 18 +++++++++ pybootchartgui/pybootchartgui/samples.py | 10 +++++ 3 files changed, 76 insertions(+) diff --git a/pybootchartgui/pybootchartgui/draw.py b/pybootchartgui/pybootchartgui/draw.py index c6e67833ab..16739a0fa1 100644 --- a/pybootchartgui/pybootchartgui/draw.py +++ b/pybootchartgui/pybootchartgui/draw.py @@ -69,6 +69,11 @@ CPU_COLOR = (0.40, 0.55, 0.70, 1.0) IO_COLOR = (0.76, 0.48, 0.48, 0.5) # Disk throughput color. DISK_TPUT_COLOR = (0.20, 0.71, 0.20, 1.0) + +BYTES_RECEIVED_COLOR = (0.0, 0.0, 1.0, 1.0) +BYTES_TRANSMITTED_COLOR = (1.0, 0.0, 0.0, 1.0) +BYTES_RECEIVE_DIFF_COLOR = (0.0, 0.0, 1.0, 0.3) +BYTES_TRANSMIT_DIFF_COLOR = (1.0, 0.0, 0.0, 0.3) # CPU load chart color. FILE_OPEN_COLOR = (0.20, 0.71, 0.71, 1.0) # Mem cached color @@ -437,6 +442,49 @@ def render_charts(ctx, options, clip, trace, curr_y, w, h, sec_w): curr_y = curr_y + 30 + bar_h + if trace.net_stats: + for iface, samples in trace.net_stats.items(): + max_received_sample = max(samples, key=lambda s: s.received_bytes) + max_transmitted_sample = max(samples, key=lambda s: s.transmitted_bytes) + max_receive_diff_sample = max(samples, key=lambda s: s.receive_diff) + max_transmit_diff_sample = max(samples, key=lambda s: s.transmit_diff) + + draw_text(ctx, "Iface: %s" % (iface), TEXT_COLOR, off_x, curr_y+20) + draw_legend_line(ctx, "Bytes received (max %d)" % (max_received_sample.received_bytes), + BYTES_RECEIVED_COLOR, off_x+150, curr_y+20, leg_s) + draw_legend_line(ctx, "Bytes transmitted (max %d)" % (max_transmitted_sample.transmitted_bytes), + BYTES_TRANSMITTED_COLOR, off_x+400, curr_y+20, leg_s) + draw_legend_box(ctx, "Bytes receive diff (max %d)" % (max_receive_diff_sample.receive_diff), + BYTES_RECEIVE_DIFF_COLOR, off_x+650, curr_y+20, leg_s) + draw_legend_box(ctx, "Bytes transmit diff (max %d)" % (max_transmit_diff_sample.transmit_diff), + BYTES_TRANSMIT_DIFF_COLOR, off_x+900, curr_y+20, leg_s) + + + chart_rect = (off_x, curr_y + 30, w, bar_h) + if clip_visible(clip, chart_rect): + draw_box_ticks(ctx, chart_rect, sec_w) + draw_annotations(ctx, proc_tree, trace.times, chart_rect) + + if clip_visible (clip, chart_rect): + draw_chart (ctx, BYTES_RECEIVED_COLOR, False, chart_rect, \ + [(sample.time, sample.received_bytes) for sample in samples], \ + proc_tree, None) + + draw_chart (ctx, BYTES_TRANSMITTED_COLOR, False, chart_rect, \ + [(sample.time, sample.transmitted_bytes) for sample in samples], \ + proc_tree, None) + + if clip_visible (clip, chart_rect): + draw_chart (ctx, BYTES_RECEIVE_DIFF_COLOR, True, chart_rect, \ + [(sample.time, sample.receive_diff) for sample in samples], \ + proc_tree, None) + + draw_chart (ctx, BYTES_TRANSMIT_DIFF_COLOR, True, chart_rect, \ + [(sample.time, sample.transmit_diff) for sample in samples], \ + proc_tree, None) + + curr_y = curr_y + 30 + bar_h + # render CPU pressure chart if trace.cpu_pressure: max_sample_avg = max (trace.cpu_pressure, key = lambda s: s.avg10) diff --git a/pybootchartgui/pybootchartgui/parsing.py b/pybootchartgui/pybootchartgui/parsing.py index 144a16c723..72a54c6ba5 100644 --- a/pybootchartgui/pybootchartgui/parsing.py +++ b/pybootchartgui/pybootchartgui/parsing.py @@ -48,6 +48,7 @@ class Trace: self.filename = None self.parent_map = None self.mem_stats = [] + self.net_stats = [] self.monitor_disk = None self.cpu_pressure = [] self.io_pressure = [] @@ -557,6 +558,21 @@ def _parse_monitor_disk_log(file): return disk_stats + +def _parse_reduced_net_log(file): + net_stats = {} + for time, lines in _parse_timed_blocks(file): + + for line in lines: + parts = line.split() + iface = parts[0][:-1] + if iface not in net_stats: + net_stats[iface] = [NetSample(time, iface, int(parts[1]), int(parts[2]), int(parts[3]), int(parts[4]))] + else: + net_stats[iface].append(NetSample(time, iface, int(parts[1]), int(parts[2]), int(parts[3]), int(parts[4]))) + return net_stats + + def _parse_pressure_logs(file, filename): """ Parse file for "some" pressure with 'avg10', 'avg60' 'avg300' and delta total values @@ -767,6 +783,8 @@ def _do_parse(writer, state, filename, file): state.cmdline = _parse_cmdline_log(writer, file) elif name == "monitor_disk.log": state.monitor_disk = _parse_monitor_disk_log(file) + elif name == "reduced_proc_net.log": + state.net_stats = _parse_reduced_net_log(file) #pressure logs are in a subdirectory elif name == "cpu.log": state.cpu_pressure = _parse_pressure_logs(file, name) diff --git a/pybootchartgui/pybootchartgui/samples.py b/pybootchartgui/pybootchartgui/samples.py index a70d8a5a28..7c92d2ce6a 100644 --- a/pybootchartgui/pybootchartgui/samples.py +++ b/pybootchartgui/pybootchartgui/samples.py @@ -37,6 +37,16 @@ class CPUSample: return str(self.time) + "\t" + str(self.user) + "\t" + \ str(self.sys) + "\t" + str(self.io) + "\t" + str (self.swap) + +class NetSample: + def __init__(self, time, iface, received_bytes, transmitted_bytes, receive_diff, transmit_diff): + self.time = time + self.iface = iface + self.received_bytes = received_bytes + self.transmitted_bytes = transmitted_bytes + self.receive_diff = receive_diff + self.transmit_diff = transmit_diff + class CPUPressureSample: def __init__(self, time, avg10, avg60, avg300, deltaTotal): self.time = time