From e0c272daf3a1ce805777f799f95f5664cbee094b Mon Sep 17 00:00:00 2001 From: Roland Schatz Date: Thu, 18 Apr 2024 19:28:30 +0200 Subject: [PATCH 01/18] Fix multi-line logs in LinesOutputCapture. --- src/mx/_impl/mx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mx/_impl/mx.py b/src/mx/_impl/mx.py index 0c53e3a3..2e69cdad 100755 --- a/src/mx/_impl/mx.py +++ b/src/mx/_impl/mx.py @@ -9271,7 +9271,7 @@ def __init__(self): self.lines = [] def __call__(self, data): - self.lines.append(data.rstrip()) + self.lines.extend(data.rstrip().split(os.linesep)) def __repr__(self): return os.linesep.join(self.lines) From 7ab4460139ef4fbd64ce780c63b07dc1f34ffc69 Mon Sep 17 00:00:00 2001 From: Roland Schatz Date: Thu, 18 Apr 2024 16:59:05 +0200 Subject: [PATCH 02/18] Funnel compiler daemon output through mx.log to play nicely with the concice logging output. --- .../mxtool/compilerserver/CompilerDaemon.java | 22 +++++++++++++------ .../mxtool/compilerserver/ECJDaemon.java | 4 ++-- .../mxtool/compilerserver/JavacDaemon.java | 7 +++--- src/mx/_impl/mx.py | 20 +++++++++++------ 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/java/com.oracle.mxtool.compilerserver/src/com/oracle/mxtool/compilerserver/CompilerDaemon.java b/java/com.oracle.mxtool.compilerserver/src/com/oracle/mxtool/compilerserver/CompilerDaemon.java index 2eb364f5..c1ecc37a 100644 --- a/java/com.oracle.mxtool.compilerserver/src/com/oracle/mxtool/compilerserver/CompilerDaemon.java +++ b/java/com.oracle.mxtool.compilerserver/src/com/oracle/mxtool/compilerserver/CompilerDaemon.java @@ -27,6 +27,7 @@ import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.OutputStreamWriter; +import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; @@ -43,6 +44,7 @@ public abstract class CompilerDaemon { // These values are used in mx.py so keep in sync. public static final String REQUEST_HEADER_COMPILE = "MX DAEMON/COMPILE: "; public static final String REQUEST_HEADER_SHUTDOWN = "MX DAEMON/SHUTDOWN"; + public static final String RESPONSE_DONE = "MX DAEMON/DONE:"; /** * The deamon will shut down after receiving this many requests with an unrecognized header. @@ -116,7 +118,7 @@ private static void usage() { abstract Compiler createCompiler(); interface Compiler { - int compile(String[] args) throws Exception; + int compile(String[] args, PrintWriter out) throws Exception; } public class Connection implements Runnable { @@ -165,14 +167,20 @@ public void run() { String[] args = commandLine.split("\u0000"); logf("%sCompiling %s%n", prefix, String.join(" ", args)); - int result = compiler.compile(args); - if (result != 0 && args.length != 0 && args[0].startsWith("GET / HTTP")) { - // GR-52712 - System.err.printf("%sFailing compilation received on %s%n", prefix, connectionSocket); + int result; + PrintWriter log = new PrintWriter(output); + try { + result = compiler.compile(args, log); + if (result != 0 && args.length != 0 && args[0].startsWith("GET / HTTP")) { + // GR-52712 + System.err.printf("%sFailing compilation received on %s%n", prefix, connectionSocket); + } + } finally { + log.flush(); } logf("%sResult = %d%n", prefix, result); - output.write(result + "\n"); + output.write(RESPONSE_DONE + result + "\n"); } else { int unrecognizedRequestCount = unrecognizedRequests.incrementAndGet(); System.err.printf("%sUnrecognized request %d (len=%d): \"%s\"%n", prefix, unrecognizedRequestCount, request.length(), printable(request)); @@ -180,7 +188,7 @@ public void run() { System.err.printf("%sShutting down after receiving %d unrecognized requests%n", prefix, unrecognizedRequestCount); System.exit(0); } - output.write("-1\n"); + output.write(RESPONSE_DONE + "-1\n"); } } finally { // close IO streams, then socket diff --git a/java/com.oracle.mxtool.compilerserver/src/com/oracle/mxtool/compilerserver/ECJDaemon.java b/java/com.oracle.mxtool.compilerserver/src/com/oracle/mxtool/compilerserver/ECJDaemon.java index 20715bd5..81743895 100644 --- a/java/com.oracle.mxtool.compilerserver/src/com/oracle/mxtool/compilerserver/ECJDaemon.java +++ b/java/com.oracle.mxtool.compilerserver/src/com/oracle/mxtool/compilerserver/ECJDaemon.java @@ -30,8 +30,8 @@ public class ECJDaemon extends CompilerDaemon { private final class ECJCompiler implements Compiler { - public int compile(String[] args) throws Exception { - boolean result = (Boolean) compileMethod.invoke(null, args, new PrintWriter(System.out), new PrintWriter(System.err), null); + public int compile(String[] args, PrintWriter out) throws Exception { + boolean result = (Boolean) compileMethod.invoke(null, args, out, out, null); return result ? 0 : -1; } } diff --git a/java/com.oracle.mxtool.compilerserver/src/com/oracle/mxtool/compilerserver/JavacDaemon.java b/java/com.oracle.mxtool.compilerserver/src/com/oracle/mxtool/compilerserver/JavacDaemon.java index e9ea6b0f..bcb23b45 100644 --- a/java/com.oracle.mxtool.compilerserver/src/com/oracle/mxtool/compilerserver/JavacDaemon.java +++ b/java/com.oracle.mxtool.compilerserver/src/com/oracle/mxtool/compilerserver/JavacDaemon.java @@ -24,14 +24,15 @@ */ package com.oracle.mxtool.compilerserver; +import java.io.PrintWriter; import java.lang.reflect.Method; public class JavacDaemon extends CompilerDaemon { private final class JavacCompiler implements Compiler { - public int compile(String[] args) throws Exception { + public int compile(String[] args, PrintWriter out) throws Exception { final Object receiver = javacMainClass.getDeclaredConstructor().newInstance(); - int result = (Integer) compileMethod.invoke(receiver, (Object) args); + int result = (Integer) compileMethod.invoke(receiver, args, out); if (result != 0 && result != 1) { // @formatter:off /* @@ -56,7 +57,7 @@ public int compile(String[] args) throws Exception { JavacDaemon() throws Exception { this.javacMainClass = Class.forName("com.sun.tools.javac.Main"); - this.compileMethod = javacMainClass.getMethod("compile", String[].class); + this.compileMethod = javacMainClass.getMethod("compile", String[].class, PrintWriter.class); } @Override diff --git a/src/mx/_impl/mx.py b/src/mx/_impl/mx.py index 2e69cdad..3f9587d8 100755 --- a/src/mx/_impl/mx.py +++ b/src/mx/_impl/mx.py @@ -7776,8 +7776,10 @@ def _noticePort(self, data): # See: # com.oracle.mxtool.compilerserver.CompilerDaemon.REQUEST_HEADER_COMPILE # com.oracle.mxtool.compilerserver.CompilerDaemon.REQUEST_HEADER_SHUTDOWN + # com.oracle.mxtool.compilerserver.CompilerDaemon.RESPONSE_DONE header_compile = "MX DAEMON/COMPILE: " header_shutdown = "MX DAEMON/SHUTDOWN" + response_done = "MX DAEMON/DONE:" def compile(self, compilerArgs): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -7786,13 +7788,17 @@ def compile(self, compilerArgs): commandLine = u'\x00'.join(compilerArgs) s.send((f'{CompilerDaemon.header_compile}{commandLine}\n').encode('utf-8')) f = s.makefile() - response = str(f.readline()) - if response == '': - # Compiler server process probably crashed - log('[Compiler daemon process appears to have crashed. ]') - retcode = -1 - else: - retcode = int(response) + while True: + response = str(f.readline()) + if response.startswith(CompilerDaemon.response_done): + retcode = int(response[len(CompilerDaemon.response_done):]) + break + if response == "": + # Compiler server process probably crashed + log('[Compiler daemon process appears to have crashed. ]') + retcode = -1 + break + log(response) s.close() if retcode: detailed_retcode = str(subprocess.CalledProcessError(retcode, f'Compile with {self.name()}: ' + ' '.join(compilerArgs))) From b92d43a1a42933ee0c48e5ebad8672bd6b5a6926 Mon Sep 17 00:00:00 2001 From: Roland Schatz Date: Thu, 18 Apr 2024 14:08:52 +0200 Subject: [PATCH 03/18] Properly shut down everything if a build task aborts. --- src/mx/_impl/build/tasks/__init__.py | 3 ++- src/mx/_impl/build/tasks/task.py | 22 ++++++++++++++++++++-- src/mx/_impl/mx.py | 10 +++++++--- src/mx/_impl/support/logging.py | 19 ++++++++++++++++++- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/mx/_impl/build/tasks/__init__.py b/src/mx/_impl/build/tasks/__init__.py index b3a7a528..887b8b55 100644 --- a/src/mx/_impl/build/tasks/__init__.py +++ b/src/mx/_impl/build/tasks/__init__.py @@ -30,10 +30,11 @@ "BuildTask", "NoOpTask", "Task", + "TaskAbortException", "TaskSequence" ] from .build import Buildable, BuildTask -from .task import Task +from .task import Task, TaskAbortException from .noop import NoOpTask from .sequence import TaskSequence diff --git a/src/mx/_impl/build/tasks/task.py b/src/mx/_impl/build/tasks/task.py index 1f97a879..91571c4b 100644 --- a/src/mx/_impl/build/tasks/task.py +++ b/src/mx/_impl/build/tasks/task.py @@ -32,13 +32,16 @@ from ..daemon import Daemon from ..suite import Dependency -from ...support.logging import nyi +from ...support.logging import nyi, setLogTask from ...support.processes import Process -__all__ = ["Task"] +__all__ = ["Task", "TaskAbortException"] Args = Namespace +class TaskAbortException(Exception): + pass + class Task(object, metaclass=ABCMeta): """A task executed during a build.""" @@ -59,6 +62,7 @@ def __init__(self, subject: Dependency, args: Args, parallelism: int): self.parallelism = parallelism self.deps = [] self.proc = None + self._exitcode = 0 def __str__(self) -> str: return nyi('__str__', self) @@ -70,6 +74,20 @@ def __repr__(self) -> str: def name(self) -> str: return self.subject.name + def enter(self): + setLogTask(self) + + def abort(self, code): + self._exitcode = code + raise TaskAbortException(code) + + @property + def exitcode(self): + if self._exitcode != 0: + return self._exitcode + else: + return self.proc.exitcode + @property def build_time(self): return getattr(self.subject, "build_time", 1) diff --git a/src/mx/_impl/mx.py b/src/mx/_impl/mx.py index 3f9587d8..03b54bad 100755 --- a/src/mx/_impl/mx.py +++ b/src/mx/_impl/mx.py @@ -365,7 +365,7 @@ import posixpath from .build.suite import Dependency, SuiteConstituent -from .build.tasks import BuildTask, NoOpTask +from .build.tasks import BuildTask, NoOpTask, TaskAbortException from .build.daemon import Daemon from .support.comparable import compare, Comparable from .support.envvars import env_var_to_bool, get_env @@ -14632,7 +14632,7 @@ def checkTasks(tasks): t.cleanSharedMemoryState() t._finished = True t._end_time = time.time() - if t.proc.exitcode != 0: + if t.exitcode != 0: failed.append(t) _removeSubprocess(t.sub) # Release the pipe file descriptors ASAP (only available on Python 3.7+) @@ -14690,7 +14690,11 @@ def executeTask(task): if not isinstance(task.proc, Thread): # Clear sub-process list cloned from parent process del _currentSubprocesses[:] - task.execute() + task.enter() + try: + task.execute() + except TaskAbortException: + pass task.pushSharedMemoryState() def depsDone(task): diff --git a/src/mx/_impl/support/logging.py b/src/mx/_impl/support/logging.py index cc74c90b..644263a0 100644 --- a/src/mx/_impl/support/logging.py +++ b/src/mx/_impl/support/logging.py @@ -37,6 +37,7 @@ "nyi", "colorize", "warn", + "setLogTask", ] import sys, signal, threading @@ -57,6 +58,17 @@ "cyan": "36", } +_logTask = threading.local() +_logTask.task = None + + +def setLogTask(task): + _logTask.task = task + + +def getLogTask(): + return _logTask.task + def _check_stdout_encoding(): # Importing here to avoid broken circular import @@ -274,7 +286,12 @@ def abort(codeOrMessage: str | int, context=None, killsig=signal.SIGTERM) -> NoR error_message = codeOrMessage error_code = 1 log_error(error_message) - raise SystemExit(error_code) + + t = getLogTask() + if t is not None: + t.abort(error_code) + else: + raise SystemExit(error_code) def abort_or_warn(message: str, should_abort: bool, context=None) -> Optional[NoReturn]: From b2e306132b580e7b3bb98be9cfc99acf6920bcda Mon Sep 17 00:00:00 2001 From: Roland Schatz Date: Mon, 30 Sep 2024 14:30:57 +0200 Subject: [PATCH 04/18] Buffer build task output and only output on failure. --- src/mx/_impl/build/tasks/build.py | 11 +++++++---- src/mx/_impl/build/tasks/task.py | 13 +++++++++++++ src/mx/_impl/mx.py | 21 ++++++++++++++++++++- src/mx/_impl/mx_native.py | 6 +++--- src/mx/_impl/support/logging.py | 15 +++++++++++++-- 5 files changed, 56 insertions(+), 10 deletions(-) diff --git a/src/mx/_impl/build/tasks/build.py b/src/mx/_impl/build/tasks/build.py index 4994e5a8..fe33e30a 100644 --- a/src/mx/_impl/build/tasks/build.py +++ b/src/mx/_impl/build/tasks/build.py @@ -181,10 +181,13 @@ def _timestamp(self) -> str: return '' def logBuild(self, reason: Optional[str] = None) -> None: + timestamp = self._timestamp() + if self.args.build_logs == 'oneline': + self.log(f'{timestamp}{self}...', echo=True, log=False) if reason: - log(self._timestamp() + f'{self}... [{reason}]') + self.log(f'{timestamp}{self}... [{reason}]') else: - log(self._timestamp() + f'{self}...') + self.log(f'{timestamp}{self}...') def logBuildDone(self, duration: float) -> None: timestamp = self._timestamp() @@ -193,10 +196,10 @@ def logBuildDone(self, duration: float) -> None: # Strip hours if 0 if durationStr.startswith('0:'): durationStr = durationStr[2:] - log(timestamp + f'{self} [duration: {duration}]') + self.log(f'{timestamp}{self} [duration: {duration}]', echo=True) def logClean(self) -> None: - log(f'Cleaning {self.name}...') + self.log(f'Cleaning {self.name}...') def logSkip(self, reason: Optional[str] = None) -> None: if reason: diff --git a/src/mx/_impl/build/tasks/task.py b/src/mx/_impl/build/tasks/task.py index 91571c4b..590986a9 100644 --- a/src/mx/_impl/build/tasks/task.py +++ b/src/mx/_impl/build/tasks/task.py @@ -28,10 +28,12 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod from argparse import Namespace +from threading import Lock from typing import Dict, Optional, MutableSequence from ..daemon import Daemon from ..suite import Dependency +from ... import mx from ...support.logging import nyi, setLogTask from ...support.processes import Process @@ -51,6 +53,8 @@ class Task(object, metaclass=ABCMeta): parallelism: int proc: Optional[Process] + consoleLock = Lock() + def __init__(self, subject: Dependency, args: Args, parallelism: int): """ :param subject: the dependency for which this task is executed @@ -62,6 +66,8 @@ def __init__(self, subject: Dependency, args: Args, parallelism: int): self.parallelism = parallelism self.deps = [] self.proc = None + self._log = mx.LinesOutputCapture() + self._echoLogs = not hasattr(args, 'build_logs') or args.build_logs == "full" self._exitcode = 0 def __str__(self) -> str: @@ -81,6 +87,13 @@ def abort(self, code): self._exitcode = code raise TaskAbortException(code) + def log(self, msg, echo=False, log=True): + if log: + self._log(msg) + if echo or self._echoLogs: + with Task.consoleLock: + print(msg.rstrip()) + @property def exitcode(self): if self._exitcode != 0: diff --git a/src/mx/_impl/mx.py b/src/mx/_impl/mx.py index 03b54bad..447e551d 100755 --- a/src/mx/_impl/mx.py +++ b/src/mx/_impl/mx.py @@ -370,7 +370,7 @@ from .support.comparable import compare, Comparable from .support.envvars import env_var_to_bool, get_env from .support.logging import abort, abort_or_warn, colorize, log, logv, logvv, log_error, nyi, warn, \ - _check_stdout_encoding + _check_stdout_encoding, getLogTask from .support.options import _opts, _opts_parsed_deferrables from .support.path import _safe_path, lstat from .support.processes import _addSubprocess, _check_output_str, _currentSubprocesses, _is_process_alive, _kill_process, _removeSubprocess, _waitWithTimeout, waitOn @@ -13406,6 +13406,13 @@ def redirect(pid, stream, f): f(line.decode()) stream.close() + t = getLogTask() + if t is not None: + if out is None: + out = t.log + if err is None: + err = t.log + stdout = out if not callable(out) else subprocess.PIPE stderr = err if not callable(err) else subprocess.PIPE stdin_pipe = None if stdin is None else subprocess.PIPE @@ -14439,6 +14446,16 @@ def build(cmd_args, parser=None): parser.add_argument('--gmake', action='store', help='path to the \'make\' executable that should be used', metavar='', default=None) parser.add_argument('--graph-file', action='store', help='path where a DOT graph of the build plan should be stored.\nIf the extension is ps, pdf, svg, png, git, or jpg, it will be rendered.', metavar='', default=None) + def get_default_build_logs(): + ret = get_env('MX_BUILD_LOGS') + if ret is not None: + return ret + if get_opts().verbose: + return "full" + else: + return "oneline" + parser.add_argument('--build-logs', action='store', choices=['silent', 'oneline', 'full'], help='what to do with log output of the individual build tasks.\nOne of silent (only print on error), oneline (print one line per task) or full (print all output).\nCan also be set with the MX_BUILD_LOGS env variable.', default=get_default_build_logs()) + compilerSelect = parser.add_mutually_exclusive_group() compilerSelect.add_argument('--error-prone', dest='error_prone', help='path to error-prone.jar', metavar='') compilerSelect.add_argument('--jdt', help='path to a stand alone Eclipse batch compiler jar (e.g. ecj.jar). ' @@ -14762,6 +14779,8 @@ def dump_task_stats(f): if len(failed): for t in failed: log_error(f'{t} failed') + for l in t._log.lines: + log(l) for daemon in daemons.values(): daemon.shutdown() abort(f'{len(failed)} build tasks failed') diff --git a/src/mx/_impl/mx_native.py b/src/mx/_impl/mx_native.py index 1216c35b..e6a5f34d 100644 --- a/src/mx/_impl/mx_native.py +++ b/src/mx/_impl/mx_native.py @@ -389,8 +389,8 @@ def _run(self, *args, **kwargs): cmd += ['-v'] cmd += args - out = kwargs.get('out', mx.OutputCapture()) - err = kwargs.get('err', subprocess.STDOUT) + out = kwargs.get('out') + err = kwargs.get('err') verbose = mx.get_opts().verbose or mx_verbose_env if verbose: if callable(out) and '-n' not in args: @@ -400,7 +400,7 @@ def _run(self, *args, **kwargs): rc = mx.run(cmd, nonZeroIsFatal=False, out=out, err=err, cwd=self.build_dir) if rc: - mx.abort(rc if verbose else (out, err)) + mx.abort(rc) class NativeDependency(mx.Dependency): diff --git a/src/mx/_impl/support/logging.py b/src/mx/_impl/support/logging.py index 644263a0..f99490c2 100644 --- a/src/mx/_impl/support/logging.py +++ b/src/mx/_impl/support/logging.py @@ -37,6 +37,7 @@ "nyi", "colorize", "warn", + "getLogTask", "setLogTask", ] @@ -59,7 +60,6 @@ } _logTask = threading.local() -_logTask.task = None def setLogTask(task): @@ -67,6 +67,8 @@ def setLogTask(task): def getLogTask(): + if not hasattr(_logTask, "task"): + return None return _logTask.task @@ -133,6 +135,10 @@ def log(msg: Optional[str] = None, end: Optional[str] = "\n"): All script output goes through this method thus allowing a subclass to redirect it. """ + task = getLogTask() + if task is not None: + task.log(msg) + return if vars(_opts).get("quiet"): return if msg is None: @@ -234,7 +240,12 @@ def warn(msg: str, context=None) -> None: else: contextMsg = str(context) msg = contextMsg + ":\n" + msg - _print_impl(colorize("WARNING: " + msg, color="magenta", bright=True, stream=sys.stderr), file=sys.stderr) + msg = colorize("WARNING: " + msg, color="magenta", bright=True, stream=sys.stderr) + task = getLogTask() + if task is None: + _print_impl(msg, file=sys.stderr) + else: + task.log(msg) def abort(codeOrMessage: str | int, context=None, killsig=signal.SIGTERM) -> NoReturn: From d20db8973a75b5c2805d5333ac7fbbc0e04d6f05 Mon Sep 17 00:00:00 2001 From: Roland Schatz Date: Mon, 30 Sep 2024 14:31:45 +0200 Subject: [PATCH 05/18] More concise status printing of mx build on interactive tty. --- src/mx/_impl/build/tasks/task.py | 6 +++ src/mx/_impl/mx.py | 80 ++++++++++++++++++++++++++------ src/mx/_impl/support/logging.py | 4 +- 3 files changed, 76 insertions(+), 14 deletions(-) diff --git a/src/mx/_impl/build/tasks/task.py b/src/mx/_impl/build/tasks/task.py index 590986a9..87a2e9a5 100644 --- a/src/mx/_impl/build/tasks/task.py +++ b/src/mx/_impl/build/tasks/task.py @@ -94,6 +94,12 @@ def log(self, msg, echo=False, log=True): with Task.consoleLock: print(msg.rstrip()) + def getLastLogLine(self): + for line in reversed(self._log.lines): + if line.strip(): + return line + return None + @property def exitcode(self): if self._exitcode != 0: diff --git a/src/mx/_impl/mx.py b/src/mx/_impl/mx.py index 447e551d..b8e7b80b 100755 --- a/src/mx/_impl/mx.py +++ b/src/mx/_impl/mx.py @@ -14452,9 +14452,19 @@ def get_default_build_logs(): return ret if get_opts().verbose: return "full" + elif is_continuous_integration(): + return "oneline" + elif not sys.stdout.closed and sys.stdout.isatty(): + # Note that this check is different from mx.is_interactive(). + # mx.is_interactive checks whether `stdin` is a tty, i.e. whether we can interactively ask questions. + # Here we check whether `stdout` is a tty, i.e. whether we can update a single line with \r. + return "interactive" else: return "oneline" - parser.add_argument('--build-logs', action='store', choices=['silent', 'oneline', 'full'], help='what to do with log output of the individual build tasks.\nOne of silent (only print on error), oneline (print one line per task) or full (print all output).\nCan also be set with the MX_BUILD_LOGS env variable.', default=get_default_build_logs()) + parser.add_argument('--build-logs', action='store', choices=['silent', 'oneline', 'interactive', 'full'], default=get_default_build_logs(), + help='what to do with log output of the individual build tasks.\n' + + 'One of silent (only print on error), oneline (print one line per task), interactive (print an interactive status line) or full (print all output).\n' + + 'Can also be set with the MX_BUILD_LOGS env variable.') compilerSelect = parser.add_mutually_exclusive_group() compilerSelect.add_argument('--error-prone', dest='error_prone', help='path to error-prone.jar', metavar='') @@ -14626,17 +14636,6 @@ def _registerDep(src, dst, edge): daemons = {} if args.parallelize and onlyDeps is None: _before_fork() - def joinTasks(tasks): - failed = [] - for t in tasks: - t.proc.join() - _removeSubprocess(t.sub) - if t.proc.exitcode != 0: - failed.append(t) - # Release the pipe file descriptors ASAP (only available on Python 3.7+) - if hasattr(t.proc, 'close'): - t.proc.close() - return failed def checkTasks(tasks): active = [] @@ -14679,6 +14678,7 @@ def sortWorklist(tasks): def anyJavaTask(tasks): return any(isinstance(task, (JavaBuildTask, JARArchiveTask)) for task in tasks) + totalTasks = len(sortedTasks) worklist = sortWorklist(sortedTasks) active = [] failed = [] @@ -14689,11 +14689,63 @@ def _activeCpus(_active): cpus += t.parallelism return cpus + progressIdx = 0 + curProgressTask = None + isInteractive = args.build_logs == "interactive" + def showProgress(): + if not isInteractive or totalTasks < 2: + return + running = len(active) + pending = len(worklist) + err = len(failed) + done = totalTasks - pending - running - err + statusline = "" + if err > 0: + statusline += f"[{colorize(str(err), color='red')}/" + else: + statusline += "[" + statusline += f"{colorize(str(running), color='blue')}/{colorize(str(done), color='green')}/{totalTasks}]" + if running > 0: + nonlocal progressIdx, curProgressTask + if curProgressTask not in active: + curProgressTask = active[0] + elif progressIdx % 5 == 0: + idx = active.index(curProgressTask) + curProgressTask = active[(idx + 1) % running] + progressIdx += 1 + statusline += f" {curProgressTask}" + lastLine = curProgressTask.getLastLogLine() + if lastLine: + statusline += " | " + lastLine + columns = shutil.get_terminal_size(fallback=(120,50)).columns + aligned = statusline + columns * " " + aligned = aligned[0:columns] + sys.stdout.write(aligned + "\r") + if done == totalTasks: + sys.stdout.write(statusline + " done\n") + elif running == 0 and err > 0: + sys.stdout.write(statusline + " failed\n") + + def joinTasks(): + for t in active[:]: + showProgress() + t.proc.join(timeout=0.2 if isInteractive else None) + if t.proc.exitcode is None: + continue + active.remove(t) + _removeSubprocess(t.sub) + if t.proc.exitcode != 0: + failed.append(t) + # Release the pipe file descriptors ASAP (only available on Python 3.7+) + if hasattr(t.proc, 'close'): + t.proc.close() + while len(worklist) != 0: while True: active, failed = checkTasks(active) if len(failed) != 0: break + showProgress() if _activeCpus(active) >= cpus: # Sleep for 0.2 second time.sleep(0.2) @@ -14750,7 +14802,9 @@ def depsDone(task): worklist = sortWorklist(worklist) - failed += joinTasks(active) + while len(active) > 0: + joinTasks() + showProgress() def dump_task_stats(f): """ diff --git a/src/mx/_impl/support/logging.py b/src/mx/_impl/support/logging.py index f99490c2..56165b09 100644 --- a/src/mx/_impl/support/logging.py +++ b/src/mx/_impl/support/logging.py @@ -296,12 +296,14 @@ def abort(codeOrMessage: str | int, context=None, killsig=signal.SIGTERM) -> NoR else: error_message = codeOrMessage error_code = 1 - log_error(error_message) t = getLogTask() if t is not None: + if error_message: + t.log(error_message) t.abort(error_code) else: + log_error(error_message) raise SystemExit(error_code) From 4ceb60b9dba5009d7d07f7377daadde7c425ec97 Mon Sep 17 00:00:00 2001 From: Roland Schatz Date: Mon, 30 Sep 2024 20:09:21 +0200 Subject: [PATCH 06/18] Cancel pending build subprocesses on failure. --- src/mx/_impl/build/tasks/task.py | 15 +++++++++++++++ src/mx/_impl/mx.py | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/src/mx/_impl/build/tasks/task.py b/src/mx/_impl/build/tasks/task.py index 87a2e9a5..e3ba9ca9 100644 --- a/src/mx/_impl/build/tasks/task.py +++ b/src/mx/_impl/build/tasks/task.py @@ -66,6 +66,7 @@ def __init__(self, subject: Dependency, args: Args, parallelism: int): self.parallelism = parallelism self.deps = [] self.proc = None + self.subprocs = [] self._log = mx.LinesOutputCapture() self._echoLogs = not hasattr(args, 'build_logs') or args.build_logs == "full" self._exitcode = 0 @@ -100,6 +101,20 @@ def getLastLogLine(self): return line return None + def addSubproc(self, p): + self.subprocs += [p] + + def cancelSubprocs(self): + from ...support.processes import _is_process_alive, _kill_process + from signal import SIGTERM + for p in self.subprocs: + if not _is_process_alive(p): + continue + if mx.is_windows(): + p.terminate() + else: + _kill_process(p.pid, SIGTERM) + @property def exitcode(self): if self._exitcode != 0: diff --git a/src/mx/_impl/mx.py b/src/mx/_impl/mx.py index b8e7b80b..73534db6 100755 --- a/src/mx/_impl/mx.py +++ b/src/mx/_impl/mx.py @@ -13421,6 +13421,9 @@ def redirect(pid, stream, f): p = subprocess.Popen(cmd_line if is_windows() else args, cwd=cwd, stdout=stdout, stderr=stderr, start_new_session=start_new_session, creationflags=creationflags, env=env, stdin=stdin_pipe, **kwargs) sub = _addSubprocess(p, args) + if t is not None: + t.addSubproc(p) + joiners = [] if callable(out): t = Thread(target=redirect, args=(p.pid, p.stdout, out)) @@ -14802,6 +14805,11 @@ def depsDone(task): worklist = sortWorklist(worklist) + if len(failed) > 0: + # cancel pending build subprocesses on failure + for t in active: + t.cancelSubprocs() + while len(active) > 0: joinTasks() showProgress() From a4730e5a6addf1932fc9e3cd6f4aa5878fb92fcb Mon Sep 17 00:00:00 2001 From: Roland Schatz Date: Fri, 19 Apr 2024 11:32:50 +0200 Subject: [PATCH 07/18] Handle subtasks in statusline. --- src/mx/_impl/mx.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/mx/_impl/mx.py b/src/mx/_impl/mx.py index 73534db6..6a0d44e8 100755 --- a/src/mx/_impl/mx.py +++ b/src/mx/_impl/mx.py @@ -365,7 +365,7 @@ import posixpath from .build.suite import Dependency, SuiteConstituent -from .build.tasks import BuildTask, NoOpTask, TaskAbortException +from .build.tasks import BuildTask, NoOpTask, TaskAbortException, TaskSequence from .build.daemon import Daemon from .support.comparable import compare, Comparable from .support.envvars import env_var_to_bool, get_env @@ -14695,6 +14695,7 @@ def _activeCpus(_active): progressIdx = 0 curProgressTask = None isInteractive = args.build_logs == "interactive" + subTaskIdx = 0 def showProgress(): if not isInteractive or totalTasks < 2: return @@ -14709,15 +14710,27 @@ def showProgress(): statusline += "[" statusline += f"{colorize(str(running), color='blue')}/{colorize(str(done), color='green')}/{totalTasks}]" if running > 0: - nonlocal progressIdx, curProgressTask + nonlocal progressIdx, curProgressTask, subTaskIdx if curProgressTask not in active: curProgressTask = active[0] + subTaskIdx = 0 elif progressIdx % 5 == 0: - idx = active.index(curProgressTask) - curProgressTask = active[(idx + 1) % running] + switchToNext = True + if isinstance(curProgressTask, TaskSequence): + subTaskIdx += 1 + if subTaskIdx >= len(curProgressTask.subtasks): + subTaskIdx = 0 + else: + switchToNext = False + if switchToNext: + idx = active.index(curProgressTask) + curProgressTask = active[(idx + 1) % running] progressIdx += 1 - statusline += f" {curProgressTask}" - lastLine = curProgressTask.getLastLogLine() + t = curProgressTask + if isinstance(t, TaskSequence): + t = t.subtasks[subTaskIdx] + statusline += f" {t}" + lastLine = t.getLastLogLine() if lastLine: statusline += " | " + lastLine columns = shutil.get_terminal_size(fallback=(120,50)).columns From 52afc52003038ee3eb16206beea1bc116e44cfd7 Mon Sep 17 00:00:00 2001 From: Roland Schatz Date: Tue, 1 Oct 2024 14:17:08 +0200 Subject: [PATCH 08/18] Include download progress in build logs infrastructure. --- src/mx/_impl/build/tasks/task.py | 10 +++++++++- src/mx/_impl/mx.py | 15 ++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/mx/_impl/build/tasks/task.py b/src/mx/_impl/build/tasks/task.py index e3ba9ca9..d5993bf0 100644 --- a/src/mx/_impl/build/tasks/task.py +++ b/src/mx/_impl/build/tasks/task.py @@ -88,8 +88,16 @@ def abort(self, code): self._exitcode = code raise TaskAbortException(code) - def log(self, msg, echo=False, log=True): + def log(self, msg, echo=False, log=True, replace=False): + """ + Log output for this build task. `echo=True` forces the output to go to the terminal regardless + of the --build-logs setting. `log=False` can be used to only do output, without including it + in the log. `replace=True` replaces the last logged line. This is useful for status output, e.g. + download progress. + """ if log: + if replace: + del self._log.lines[-1] self._log(msg) if echo or self._echoLogs: with Task.consoleLock: diff --git a/src/mx/_impl/mx.py b/src/mx/_impl/mx.py index 6a0d44e8..f0a3b7fe 100755 --- a/src/mx/_impl/mx.py +++ b/src/mx/_impl/mx.py @@ -4245,6 +4245,9 @@ def _attempt_download(url, path, jarEntryName=None): bytesRead = 0 chunkSize = 8192 + if progress: + logTask = getLogTask() + with open(tmp, 'wb') as fp: chunk = conn.read(chunkSize) while chunk: @@ -4252,15 +4255,21 @@ def _attempt_download(url, path, jarEntryName=None): fp.write(chunk) if length == -1: if progress: - sys.stdout.write(f'\r {bytesRead} bytes') + if logTask: + logTask.log(f'{bytesRead} bytes', replace=True) + else: + sys.stdout.write(f'\r {bytesRead} bytes') else: if progress: - sys.stdout.write(f'\r {bytesRead} bytes ({bytesRead * 100 / length:.0f}%)') + if logTask: + logTask.log(f'{bytesRead} bytes ({bytesRead * 100 / length:.0f}%)', replace=True) + else: + sys.stdout.write(f'\r {bytesRead} bytes ({bytesRead * 100 / length:.0f}%)') if bytesRead == length: break chunk = conn.read(chunkSize) - if progress: + if progress and not logTask: sys.stdout.write('\n') if length not in (-1, bytesRead): From ae57a368c1bd07a1aebb3d185dfad2cee4fe99c5 Mon Sep 17 00:00:00 2001 From: Roland Schatz Date: Wed, 6 Nov 2024 14:16:08 +0100 Subject: [PATCH 09/18] Produce html build report. --- src/mx/_impl/build/report.py | 71 +++++++++++++++++++++++++++++++ src/mx/_impl/build/tasks/build.py | 2 + src/mx/_impl/build/tasks/task.py | 9 ++++ src/mx/_impl/mx.py | 14 ++++++ 4 files changed, 96 insertions(+) create mode 100644 src/mx/_impl/build/report.py diff --git a/src/mx/_impl/build/report.py b/src/mx/_impl/build/report.py new file mode 100644 index 00000000..887fb872 --- /dev/null +++ b/src/mx/_impl/build/report.py @@ -0,0 +1,71 @@ +# +# ---------------------------------------------------------------------------------------------------- +# +# Copyright (c) 2024, 2024, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# This code is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 2 only, as +# published by the Free Software Foundation. +# +# This code is distributed in the hope that 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 +# version 2 for more details (a copy is included in the LICENSE file that +# accompanied this code). +# +# You should have received a copy of the GNU General Public License version +# 2 along with this work; if not, write to the Free Software Foundation, +# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. +# +# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA +# or visit www.oracle.com if you need additional information or have any +# questions. +# +# ---------------------------------------------------------------------------------------------------- +# + +import html + +def write_task_report(f, task): + f.write('\n
\n') + f.write(f'

{task.name}

\n') + f.write(f'

{task.statusInfo}

\n') + l = str(task._log).strip() + if l: + f.write('
\n')
+        f.write(html.escape(l))
+        f.write('\n        
\n') + f.write('
\n') + +def write_style(f): + f.write(''' + + ''') + +def write_build_report(filename, tasks): + allSkipped = True + for t in tasks: + if t.status != "skipped": + allSkipped = False + break + if allSkipped: + # don't bother writing a build log if there was nothing to do + return + with open(filename, 'w', encoding='utf-8') as f: + f.write('\n') + f.write('\n') + f.write('\n') + for t in tasks: + if t.status is not None: + # only report on tasks that were started + write_task_report(f, t) + write_style(f) + f.write('\n') + f.write('\n') diff --git a/src/mx/_impl/build/tasks/build.py b/src/mx/_impl/build/tasks/build.py index fe33e30a..bf73dd92 100644 --- a/src/mx/_impl/build/tasks/build.py +++ b/src/mx/_impl/build/tasks/build.py @@ -202,7 +202,9 @@ def logClean(self) -> None: self.log(f'Cleaning {self.name}...') def logSkip(self, reason: Optional[str] = None) -> None: + self.status = "skipped" if reason: + self.statusInfo = reason logv(f'[{reason} - skipping {self.name}]') else: logv(f'[skipping {self.name}]') diff --git a/src/mx/_impl/build/tasks/task.py b/src/mx/_impl/build/tasks/task.py index d5993bf0..4064139f 100644 --- a/src/mx/_impl/build/tasks/task.py +++ b/src/mx/_impl/build/tasks/task.py @@ -70,6 +70,8 @@ def __init__(self, subject: Dependency, args: Args, parallelism: int): self._log = mx.LinesOutputCapture() self._echoLogs = not hasattr(args, 'build_logs') or args.build_logs == "full" self._exitcode = 0 + self.status = None + self.statusInfo = "" def __str__(self) -> str: return nyi('__str__', self) @@ -82,10 +84,17 @@ def name(self) -> str: return self.subject.name def enter(self): + self.status = "running" setLogTask(self) + def leave(self): + if self.status == "running": + self.status = "success" + setLogTask(None) + def abort(self, code): self._exitcode = code + self.status = "failed" raise TaskAbortException(code) def log(self, msg, echo=False, log=True, replace=False): diff --git a/src/mx/_impl/mx.py b/src/mx/_impl/mx.py index f0a3b7fe..f6be7247 100755 --- a/src/mx/_impl/mx.py +++ b/src/mx/_impl/mx.py @@ -367,6 +367,7 @@ from .build.suite import Dependency, SuiteConstituent from .build.tasks import BuildTask, NoOpTask, TaskAbortException, TaskSequence from .build.daemon import Daemon +from .build.report import write_build_report from .support.comparable import compare, Comparable from .support.envvars import env_var_to_bool, get_env from .support.logging import abort, abort_or_warn, colorize, log, logv, logvv, log_error, nyi, warn, \ @@ -8754,6 +8755,7 @@ def logBuild(self, reason=None): pass def logSkip(self, reason=None): + self.status = "skipped" pass def needsBuild(self, newestInput): @@ -14789,6 +14791,8 @@ def executeTask(task): task.execute() except TaskAbortException: pass + finally: + task.leave() task.pushSharedMemoryState() def depsDone(task): @@ -14836,6 +14840,16 @@ def depsDone(task): joinTasks() showProgress() + reportDir = primary_suite().get_output_root(jdkDependent=False) + ensure_dir_exists(reportDir) + base_name = time.strftime("buildlog-%Y%m%d-%H%M%S") + reportFile = os.path.join(reportDir, base_name + ".html") + reportIdx = 0 + while os.path.exists(reportFile): + reportIdx += 1 + reportFile = os.path.join(reportDir, f'{base_name}_{reportIdx}.html') + write_build_report(reportFile, sortedTasks) + def dump_task_stats(f): """ Dump task statistics CSV. Use R with following commands for visualization: From 323958e929e2b298fe110deb0305dad4a0894948 Mon Sep 17 00:00:00 2001 From: Roland Schatz Date: Mon, 11 Nov 2024 16:23:15 +0100 Subject: [PATCH 10/18] Delete old build logs on `mx clean`. --- src/mx/_impl/mx.py | 13 +++++++++++++ src/mx/_impl/mx_gate.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/mx/_impl/mx.py b/src/mx/_impl/mx.py index f6be7247..2a13dd4b 100755 --- a/src/mx/_impl/mx.py +++ b/src/mx/_impl/mx.py @@ -4149,6 +4149,9 @@ def clean(args, parser=None): parser.add_argument('--all', action='store_true', help='clear all dependencies (not just default targets)') parser.add_argument('--aggressive', action='store_true', help='clear all suite output') parser.add_argument('--disk-usage', action='store_true', help='compute and show disk usage before and after') + parser.add_argument('--keep-logs', action='store_true', + help='keep old build logs (default: delete them unless in a CI job)', + default=is_continuous_integration()) args = parser.parse_args(args) @@ -4157,6 +4160,16 @@ def clean(args, parser=None): if args.disk_usage: disk_usage = {s:mx_gc._get_size_in_bytes(root) for s, root in suite_roots.items()} + if not args.keep_logs: + # Delete old build logs unless specifically told to keep them. + # Off the CI, on local dev machines, we don't want build logs accumulating infinitely. + # On the CI, this defaults to "keep the logs". We're assuming that `mxbuild` directories on the CI + # are ephemeral anyway, and we never want to lose logs when running complex jobs on the CI that + # might contain `mx clean` in the middle. + for root in suite_roots.values(): + for buildlog in glob.iglob(os.path.join(root, 'buildlog-*.html')): + os.unlink(buildlog) + def _collect_clean_dependencies(): if args.all: return dependencies(True) diff --git a/src/mx/_impl/mx_gate.py b/src/mx/_impl/mx_gate.py index ed522aa2..a17ab5fd 100644 --- a/src/mx/_impl/mx_gate.py +++ b/src/mx/_impl/mx_gate.py @@ -747,7 +747,7 @@ def _run_gate(cleanArgs, args, tasks): mx.command_function('build')(defaultBuildArgs + args.extra_build_args) fullbuild = True if Task.tags is None else Tags.fullbuild in Task.tags # pylint: disable=unsupported-membership-test if fullbuild: - gate_clean(cleanArgs, tasks, name='CleanAfterEcjBuild', tags=[Tags.fullbuild]) + gate_clean(cleanArgs + ['--keep-logs'], tasks, name='CleanAfterEcjBuild', tags=[Tags.fullbuild]) with Task('BuildWithJavac', tasks, tags=[Tags.build, Tags.fullbuild], legacyTitles=['BuildJavaWithJavac']) as t: if t: From a495ff35c5c79f1548b4bce8aba1b5cdd8b5ac11 Mon Sep 17 00:00:00 2001 From: Roland Schatz Date: Tue, 12 Nov 2024 11:31:52 +0100 Subject: [PATCH 11/18] Output path of buildlog files to support capturing them in CI. --- src/mx/_impl/build/report.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/mx/_impl/build/report.py b/src/mx/_impl/build/report.py index 887fb872..28ac2ea6 100644 --- a/src/mx/_impl/build/report.py +++ b/src/mx/_impl/build/report.py @@ -27,6 +27,8 @@ import html +from ..support.logging import log + def write_task_report(f, task): f.write('\n
\n') f.write(f'

{task.name}

\n') @@ -69,3 +71,4 @@ def write_build_report(filename, tasks): write_style(f) f.write('\n') f.write('\n') + log(f"mx build log written to {filename}") From b0584d7d65ad216404b99b5b26e4b19e878bd7b5 Mon Sep 17 00:00:00 2001 From: Roland Schatz Date: Tue, 12 Nov 2024 13:01:26 +0100 Subject: [PATCH 12/18] Make sure buildlog is always written even on error. --- src/mx/_impl/build/report.py | 67 ++++++++++++++++++++++++------------ src/mx/_impl/mx.py | 19 +++++----- 2 files changed, 53 insertions(+), 33 deletions(-) diff --git a/src/mx/_impl/build/report.py b/src/mx/_impl/build/report.py index 28ac2ea6..c554043f 100644 --- a/src/mx/_impl/build/report.py +++ b/src/mx/_impl/build/report.py @@ -26,8 +26,10 @@ # import html +import os +import time -from ..support.logging import log +from .. import mx def write_task_report(f, task): f.write('\n
\n') @@ -51,24 +53,45 @@ def write_style(f): ''') -def write_build_report(filename, tasks): - allSkipped = True - for t in tasks: - if t.status != "skipped": - allSkipped = False - break - if allSkipped: - # don't bother writing a build log if there was nothing to do - return - with open(filename, 'w', encoding='utf-8') as f: - f.write('\n') - f.write('\n') - f.write('\n') - for t in tasks: - if t.status is not None: - # only report on tasks that were started - write_task_report(f, t) - write_style(f) - f.write('\n') - f.write('\n') - log(f"mx build log written to {filename}") +class BuildReport: + def __init__(self): + self.tasks = [] + + def set_tasks(self, tasks): + self.tasks = tasks + + def _write_report(self, filename): + allSkipped = True + for t in self.tasks: + if t.status != "skipped": + allSkipped = False + break + if allSkipped: + # don't bother writing a build log if there was nothing to do + return + with open(filename, 'w', encoding='utf-8') as f: + f.write('\n') + f.write('\n') + f.write('\n') + for t in self.tasks: + if t.status is not None: + # only report on tasks that were started + write_task_report(f, t) + write_style(f) + f.write('\n') + f.write('\n') + mx.log(f"mx build log written to {filename}") + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + reportDir = mx.primary_suite().get_output_root(jdkDependent=False) + mx.ensure_dir_exists(reportDir) + base_name = time.strftime("buildlog-%Y%m%d-%H%M%S") + reportFile = os.path.join(reportDir, base_name + ".html") + reportIdx = 0 + while os.path.exists(reportFile): + reportIdx += 1 + reportFile = os.path.join(reportDir, f'{base_name}_{reportIdx}.html') + self._write_report(reportFile) diff --git a/src/mx/_impl/mx.py b/src/mx/_impl/mx.py index 2a13dd4b..032aef75 100755 --- a/src/mx/_impl/mx.py +++ b/src/mx/_impl/mx.py @@ -367,7 +367,7 @@ from .build.suite import Dependency, SuiteConstituent from .build.tasks import BuildTask, NoOpTask, TaskAbortException, TaskSequence from .build.daemon import Daemon -from .build.report import write_build_report +from .build.report import BuildReport from .support.comparable import compare, Comparable from .support.envvars import env_var_to_bool, get_env from .support.logging import abort, abort_or_warn, colorize, log, logv, logvv, log_error, nyi, warn, \ @@ -14419,6 +14419,11 @@ def resolve_targets(names): def build(cmd_args, parser=None): + """builds the artifacts of one or more dependencies""" + with BuildReport() as build_report: + _build_with_report(cmd_args, build_report=build_report, parser=parser) + +def _build_with_report(cmd_args, build_report, parser=None): """builds the artifacts of one or more dependencies""" global _gmake_cmd @@ -14612,6 +14617,8 @@ def _registerDep(src, dst, edge): walk_deps(visit=_createTask, visitEdge=_registerDep, roots=roots, ignoredEdges=[DEP_EXCLUDED]) + build_report.set_tasks(sortedTasks) + if args.graph_file: ext = get_file_extension(args.graph_file) if ext in ('dot', ''): @@ -14853,16 +14860,6 @@ def depsDone(task): joinTasks() showProgress() - reportDir = primary_suite().get_output_root(jdkDependent=False) - ensure_dir_exists(reportDir) - base_name = time.strftime("buildlog-%Y%m%d-%H%M%S") - reportFile = os.path.join(reportDir, base_name + ".html") - reportIdx = 0 - while os.path.exists(reportFile): - reportIdx += 1 - reportFile = os.path.join(reportDir, f'{base_name}_{reportIdx}.html') - write_build_report(reportFile, sortedTasks) - def dump_task_stats(f): """ Dump task statistics CSV. Use R with following commands for visualization: From 7f8a2f64c43c6f2bc933d7426989107c77567087 Mon Sep 17 00:00:00 2001 From: Roland Schatz Date: Tue, 12 Nov 2024 13:03:04 +0100 Subject: [PATCH 13/18] Put more information in build report. --- src/mx/_impl/build/report.py | 27 ++++++++++++++++++++++++++- src/mx/_impl/mx.py | 15 +++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/mx/_impl/build/report.py b/src/mx/_impl/build/report.py index c554043f..90938921 100644 --- a/src/mx/_impl/build/report.py +++ b/src/mx/_impl/build/report.py @@ -45,6 +45,7 @@ def write_task_report(f, task): def write_style(f): f.write('''