All of lore.kernel.org
 help / color / mirror / Atom feed
From: Markus Lehtonen <markus.lehtonen@linux.intel.com>
To: openembedded-core@lists.openembedded.org
Subject: [PATCH 2/2] scripts/contrib: add oe-build-perf-report-email
Date: Fri, 31 Mar 2017 17:07:30 +0300	[thread overview]
Message-ID: <20170331140730.27171-3-markus.lehtonen@linux.intel.com> (raw)
In-Reply-To: <20170331140730.27171-1-markus.lehtonen@linux.intel.com>

Script for sending build perf test reports as an email. Mangles an html
report, generated by oe-build-perf-report, into a format suitable for
html emails. Supports multipart emails where a plaintext alternative can
be included in the same email.

Dependencies required to be installed on the host:
- phantomjs
- optipng

[YOCTO #10931]

Signed-off-by: Markus Lehtonen <markus.lehtonen@linux.intel.com>
---
 scripts/contrib/oe-build-perf-report-email.py | 266 ++++++++++++++++++++++++++
 scripts/lib/build_perf/scrape-html-report.js  |  56 ++++++
 2 files changed, 322 insertions(+)
 create mode 100755 scripts/contrib/oe-build-perf-report-email.py
 create mode 100644 scripts/lib/build_perf/scrape-html-report.js

diff --git a/scripts/contrib/oe-build-perf-report-email.py b/scripts/contrib/oe-build-perf-report-email.py
new file mode 100755
index 0000000..7f4274e
--- /dev/null
+++ b/scripts/contrib/oe-build-perf-report-email.py
@@ -0,0 +1,266 @@
+#!/usr/bin/python3
+#
+# Send build performance test report emails
+#
+# Copyright (c) 2017, Intel Corporation.
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms and conditions of the GNU General Public License,
+# version 2, as published by the Free Software Foundation.
+#
+# This program is distributed in the hope it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+# more details.
+#
+import argparse
+import base64
+import logging
+import os
+import pwd
+import re
+import shutil
+import smtplib
+import subprocess
+import sys
+import tempfile
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+
+
+# Setup logging
+logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
+log = logging.getLogger('oe-build-perf-report')
+
+
+# Find js scaper script
+SCRAPE_JS = os.path.join(os.path.dirname(__file__), '..', 'lib', 'build_perf',
+                         'scrape-html-report.js')
+if not os.path.isfile(SCRAPE_JS):
+    log.error("Unableto find oe-build-perf-report-scrape.js")
+    sys.exit(1)
+
+
+class ReportError(Exception):
+    """Local errors"""
+    pass
+
+
+def check_utils():
+    """Check that all needed utils are installed in the system"""
+    missing = []
+    for cmd in ('phantomjs', 'optipng'):
+        if not shutil.which(cmd):
+            missing.append(cmd)
+    if missing:
+        log.error("The following tools are missing: %s", ' '.join(missing))
+        sys.exit(1)
+
+
+def parse_args(argv):
+    """Parse command line arguments"""
+    description = """Email build perf test report"""
+    parser = argparse.ArgumentParser(
+        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+        description=description)
+
+    parser.add_argument('--debug', '-d', action='store_true',
+                        help="Verbose logging")
+    parser.add_argument('--quiet', '-q', action='store_true',
+                        help="Only print errors")
+    parser.add_argument('--to', action='append',
+                        help="Recipients of the email")
+    parser.add_argument('--subject', default="Yocto build perf test report",
+                        help="Email subject")
+    parser.add_argument('--outdir', '-o',
+                        help="Store files in OUTDIR. Can be used to preserve "
+                             "the email parts")
+    parser.add_argument('--text',
+                        help="Plain text message")
+    parser.add_argument('--html',
+                        help="HTML peport generated by oe-build-perf-report")
+    parser.add_argument('--phantomjs-args', action='append',
+                        help="Extra command line arguments passed to PhantomJS")
+
+    args = parser.parse_args(argv)
+
+    if not args.html and not args.text:
+        parser.error("Please specify --html and/or --text")
+
+    return args
+
+
+def decode_png(infile, outfile):
+    """Parse/decode/optimize png data from a html element"""
+    with open(infile) as f:
+        raw_data = f.read()
+
+    # Grab raw base64 data
+    b64_data = re.sub('^.*href="data:image/png;base64,', '', raw_data, 1)
+    b64_data = re.sub('">.+$', '', b64_data, 1)
+
+    # Replace file with proper decoded png
+    with open(outfile, 'wb') as f:
+        f.write(base64.b64decode(b64_data))
+
+    subprocess.check_output(['optipng', outfile], stderr=subprocess.STDOUT)
+
+
+def encode_png(pngfile):
+    """Encode png into a <img> html element"""
+    with open(pngfile, 'rb') as f:
+        data = f.read()
+
+    b64_data = base64.b64encode(data)
+    return '<img src="data:image/png;base64,' + b64_data.decode('utf-8') + '">\n'
+
+
+def mangle_html_report(infile, outfile, pngs):
+    """Mangle html file into a email compatible format"""
+    paste = True
+    png_dir = os.path.dirname(outfile)
+    with open(infile) as f_in:
+        with open(outfile, 'w') as f_out:
+            for line in f_in.readlines():
+                stripped = line.strip()
+                # Strip out scripts
+                if stripped == '<!--START-OF-SCRIPTS-->':
+                    paste = False
+                elif stripped == '<!--END-OF-SCRIPTS-->':
+                    paste = True
+                elif paste:
+                    if re.match('^.+href="data:image/png;base64', stripped):
+                        # Strip out encoded pngs (as they're huge in size)
+                        continue
+                    elif 'www.gstatic.com' in stripped:
+                        # HACK: drop references to external static pages
+                        continue
+
+                    # Replace charts with <img> elements
+                    match = re.match('<div id="(?P<id>\w+)"', stripped)
+                    if match and match.group('id') in pngs:
+                        #f_out.write('<img src="{}">\n'.format(match.group('id') + '.png'))
+                        png_file = os.path.join(png_dir, match.group('id') + '.png')
+                        f_out.write(encode_png(png_file))
+                    else:
+                        f_out.write(line)
+
+
+def scrape_html_report(report, outdir, phantomjs_extra_args=None):
+    """Scrape html report into a format sendable by email"""
+    tmpdir = tempfile.mkdtemp(dir='.')
+    log.debug("Using tmpdir %s for phantomjs output", tmpdir)
+
+    if not os.path.isdir(outdir):
+        os.mkdir(outdir)
+    if os.path.splitext(report)[1] not in ('.html', '.htm'):
+        raise ReportError("Invalid file extension for report, needs to be "
+                          "'.html' or '.htm'")
+
+    try:
+        log.info("Scraping HTML report with PhangomJS")
+        extra_args = phantomjs_extra_args if phantomjs_extra_args else []
+        subprocess.check_output(['phantomjs', '--debug=true'] + extra_args +
+                                [SCRAPE_JS, report, tmpdir],
+                                stderr=subprocess.STDOUT)
+
+        pngs = []
+        attachments = []
+        for fname in os.listdir(tmpdir):
+            base, ext = os.path.splitext(fname)
+            if ext == '.png':
+                log.debug("Decoding %s", fname)
+                decode_png(os.path.join(tmpdir, fname),
+                           os.path.join(outdir, fname))
+                pngs.append(base)
+                attachments.append(fname)
+            elif ext in ('.html', '.htm'):
+                report_file = fname
+            else:
+                log.warning("Unknown file extension: '%s'", ext)
+                #shutil.move(os.path.join(tmpdir, fname), outdir)
+
+        log.debug("Mangling html report file %s", report_file)
+        mangle_html_report(os.path.join(tmpdir, report_file),
+                           os.path.join(outdir, report_file), pngs)
+        return report_file, attachments
+    finally:
+        shutil.rmtree(tmpdir)
+
+def send_email(text_fn, html_fn, subject, recipients):
+    """Send email"""
+    # Generate email message
+    text_msg = html_msg = None
+    if text_fn:
+        with open(text_fn) as f:
+            text_msg = MIMEText("Yocto build performance test report.\n" +
+                                f.read(), 'plain')
+    if html_fn:
+        with open(html_fn) as f:
+            html_msg = MIMEText(f.read(), 'html')
+
+    if text_msg and html_msg:
+        msg = MIMEMultipart('alternative')
+        msg.attach(text_msg)
+        msg.attach(html_msg)
+    elif text_msg:
+        msg = text_msg
+    elif html_msg:
+        msg = html_msg
+    else:
+        raise ReportError("Neither plain text nor html body specified")
+
+    full_name = pwd.getpwuid(os.getuid()).pw_gecos.split(',')[0]
+    email = os.environ.get('EMAIL', os.getlogin())
+    msg['From'] = "{} <{}>".format(full_name, email)
+    msg['To'] = ', '.join(recipients)
+    msg['Subject'] = subject
+
+    # Send email
+    with smtplib.SMTP('localhost') as smtp:
+        smtp.send_message(msg)
+
+
+def main(argv=None):
+    """Script entry point"""
+    args = parse_args(argv)
+    if args.quiet:
+        log.setLevel(logging.ERROR)
+    if args.debug:
+        log.setLevel(logging.DEBUG)
+
+    check_utils()
+
+    if args.outdir:
+        outdir = args.outdir
+        if not os.path.exists(outdir):
+            os.mkdir(outdir)
+    else:
+        outdir = tempfile.mkdtemp(dir='.')
+
+    try:
+        log.debug("Storing email parts in %s", outdir)
+        html_report = None
+        if args.html:
+            scrape_html_report(args.html, outdir, args.phantomjs_args)
+            html_report = os.path.join(outdir, args.html)
+
+        if args.to:
+            log.info("Sending email to %s", ', '.join(args.to))
+            send_email(args.text, html_report, args.subject, args.to)
+    except subprocess.CalledProcessError as err:
+        log.error("%s, with output:\n%s", str(err), err.output.decode())
+        return 1
+    except ReportError as err:
+        log.error(err)
+        return 1
+    finally:
+        if not args.outdir:
+            log.debug("Wiping %s", outdir)
+            shutil.rmtree(outdir)
+
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/scripts/lib/build_perf/scrape-html-report.js b/scripts/lib/build_perf/scrape-html-report.js
new file mode 100644
index 0000000..05a1f57
--- /dev/null
+++ b/scripts/lib/build_perf/scrape-html-report.js
@@ -0,0 +1,56 @@
+var fs = require('fs');
+var system = require('system');
+var page = require('webpage').create();
+
+// Examine console log for message from chart drawing
+page.onConsoleMessage = function(msg) {
+    console.log(msg);
+    if (msg === "ALL CHARTS READY") {
+        window.charts_ready = true;
+    }
+    else if (msg.slice(0, 11) === "CHART READY") {
+        var chart_id = msg.split(" ")[2];
+        console.log('grabbing ' + chart_id);
+        var png_data = page.evaluate(function (chart_id) {
+            var chart_div = document.getElementById(chart_id + '_png');
+            return chart_div.outerHTML;
+        }, chart_id);
+        fs.write(args[2] + '/' + chart_id + '.png', png_data, 'w');
+    }
+};
+
+// Check command line arguments
+var args = system.args;
+if (args.length != 3) {
+    console.log("USAGE: " + args[0] + " REPORT_HTML OUT_DIR\n");
+    phantom.exit(1);
+}
+
+// Open the web page
+page.open(args[1], function(status) {
+    if (status == 'fail') {
+        console.log("Failed to open file '" + args[1] + "'");
+        phantom.exit(1);
+    }
+});
+
+// Check status every 100 ms
+interval = window.setInterval(function () {
+    //console.log('waiting');
+    if (window.charts_ready) {
+        clearTimeout(timer);
+        clearInterval(interval);
+
+        var fname = args[1].replace(/\/+$/, "").split("/").pop()
+        console.log("saving " + fname);
+        fs.write(args[2] + '/' + fname, page.content, 'w');
+        phantom.exit(0);
+    }
+}, 100);
+
+// Time-out after 10 seconds
+timer = window.setTimeout(function () {
+    clearInterval(interval);
+    console.log("ERROR: timeout");
+    phantom.exit(1);
+}, 10000);
-- 
2.10.2



      parent reply	other threads:[~2017-03-31 14:07 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2017-03-31 14:07 [PATCH 0/2] Tools for visualizing build performance test results Markus Lehtonen
2017-03-31 14:07 ` [PATCH 1/2] scripts: add oe-build-perf-report script Markus Lehtonen
2017-03-31 14:07 ` Markus Lehtonen [this message]

Reply instructions:

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

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

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

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

  git send-email \
    --in-reply-to=20170331140730.27171-3-markus.lehtonen@linux.intel.com \
    --to=markus.lehtonen@linux.intel.com \
    --cc=openembedded-core@lists.openembedded.org \
    /path/to/YOUR_REPLY

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

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