diff options
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/checkpatch.pl | 2 | ||||
| -rwxr-xr-x | scripts/decodetree.py | 2 | ||||
| -rwxr-xr-x | scripts/device-crash-test | 3 | ||||
| -rw-r--r-- | scripts/qapi/events.py | 24 | ||||
| -rwxr-xr-x | scripts/qemu-trace-stap | 175 | ||||
| -rw-r--r-- | scripts/qemu-trace-stap.texi | 140 | ||||
| -rw-r--r-- | scripts/qtest.py | 6 | ||||
| -rwxr-xr-x | scripts/render_block_graph.py | 120 | ||||
| -rw-r--r-- | scripts/tracetool/__init__.py | 6 | ||||
| -rw-r--r-- | scripts/tracetool/format/log_stap.py | 127 |
10 files changed, 592 insertions, 13 deletions
diff --git a/scripts/checkpatch.pl b/scripts/checkpatch.pl index d10dddf1be..88682cb0a9 100755 --- a/scripts/checkpatch.pl +++ b/scripts/checkpatch.pl @@ -1624,7 +1624,7 @@ sub process { # Block comments use /* on a line of its own if ($rawline !~ m@^\+.*/\*.*\*/[ \t]*$@ && #inline /*...*/ - $rawline =~ m@^\+.*/\*\*?[ \t]*.+[ \t]*$@) { # /* or /** non-blank + $rawline =~ m@^\+.*/\*\*?+[ \t]*[^ \t]@) { # /* or /** non-blank WARN("Block comments use a leading /* on a separate line\n" . $herecurr); } diff --git a/scripts/decodetree.py b/scripts/decodetree.py index 0bc73b5990..e342d278b8 100755 --- a/scripts/decodetree.py +++ b/scripts/decodetree.py @@ -204,7 +204,7 @@ def output(*args): output_fd.write(a) -if sys.version_info >= (3, 0): +if sys.version_info >= (3, 4): re_fullmatch = re.fullmatch else: def re_fullmatch(pat, str): diff --git a/scripts/device-crash-test b/scripts/device-crash-test index 483dafb2fc..2a13fa4f84 100755 --- a/scripts/device-crash-test +++ b/scripts/device-crash-test @@ -557,7 +557,8 @@ def main(): logger.info("Skipped %d test cases", skipped) if args.debug: - stats = sorted([(len(wl_stats.get(i, [])), wl) for i, wl in enumerate(ERROR_WHITELIST)]) + stats = sorted([(len(wl_stats.get(i, [])), wl) for i, wl in + enumerate(ERROR_WHITELIST)], key=lambda x: x[0]) for count, wl in stats: dbg("whitelist entry stats: %d: %r", count, wl) diff --git a/scripts/qapi/events.py b/scripts/qapi/events.py index 37ee5de682..d86a2d2b3e 100644 --- a/scripts/qapi/events.py +++ b/scripts/qapi/events.py @@ -58,7 +58,7 @@ def gen_param_var(typ): return ret -def gen_event_send(name, arg_type, boxed, event_enum_name): +def gen_event_send(name, arg_type, boxed, event_enum_name, event_emit): # FIXME: Our declaration of local variables (and of 'errp' in the # parameter list) can collide with exploded members of the event's # data type passed in as parameters. If this collision ever hits in @@ -70,7 +70,6 @@ def gen_event_send(name, arg_type, boxed, event_enum_name): %(proto)s { QDict *qmp; - QMPEventFuncEmit emit; ''', proto=build_event_send_proto(name, arg_type, boxed)) @@ -86,11 +85,6 @@ def gen_event_send(name, arg_type, boxed, event_enum_name): ret += mcgen(''' - emit = qmp_event_get_func_emit(); - if (!emit) { - return; - } - qmp = qmp_event_build_dict("%(name)s"); ''', @@ -121,9 +115,10 @@ def gen_event_send(name, arg_type, boxed, event_enum_name): ''') ret += mcgen(''' - emit(%(c_enum)s, qmp); + %(event_emit)s(%(c_enum)s, qmp); ''', + event_emit=event_emit, c_enum=c_enum_const(event_enum_name, name)) if arg_type and not arg_type.is_empty(): @@ -145,6 +140,7 @@ class QAPISchemaGenEventVisitor(QAPISchemaModularCVisitor): ' * Schema-defined QAPI/QMP events', __doc__) self._event_enum_name = c_name(prefix + 'QAPIEvent', protect=False) self._event_enum_members = [] + self._event_emit_name = c_name(prefix + 'qapi_event_emit') def _begin_module(self, name): types = self._module_basename('qapi-types', name) @@ -170,15 +166,23 @@ class QAPISchemaGenEventVisitor(QAPISchemaModularCVisitor): def visit_end(self): (genc, genh) = self._module[self._main_module] - genh.add(gen_enum(self._event_enum_name, self._event_enum_members)) + genh.add(gen_enum(self._event_enum_name, + self._event_enum_members)) genc.add(gen_enum_lookup(self._event_enum_name, self._event_enum_members)) + genh.add(mcgen(''' + +void %(event_emit)s(%(event_enum)s event, QDict *qdict); +''', + event_emit=self._event_emit_name, + event_enum=self._event_enum_name)) def visit_event(self, name, info, ifcond, arg_type, boxed): with ifcontext(ifcond, self._genh, self._genc): self._genh.add(gen_event_send_decl(name, arg_type, boxed)) self._genc.add(gen_event_send(name, arg_type, boxed, - self._event_enum_name)) + self._event_enum_name, + self._event_emit_name)) self._event_enum_members.append(QAPISchemaMember(name, ifcond)) diff --git a/scripts/qemu-trace-stap b/scripts/qemu-trace-stap new file mode 100755 index 0000000000..91d1051cdc --- /dev/null +++ b/scripts/qemu-trace-stap @@ -0,0 +1,175 @@ +#!/usr/bin/python +# -*- python -*- +# +# Copyright (C) 2019 Red Hat, Inc +# +# QEMU SystemTap Trace Tool +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see <http://www.gnu.org/licenses/>. + +from __future__ import print_function + +import argparse +import copy +import os.path +import re +import subprocess +import sys + + +def probe_prefix(binary): + dirname, filename = os.path.split(binary) + return re.sub("-", ".", filename) + ".log" + + +def which(binary): + for path in os.environ["PATH"].split(os.pathsep): + if os.path.exists(os.path.join(path, binary)): + return os.path.join(path, binary) + + print("Unable to find '%s' in $PATH" % binary) + sys.exit(1) + + +def tapset_dir(binary): + dirname, filename = os.path.split(binary) + if dirname == '': + thisfile = which(binary) + else: + thisfile = os.path.realpath(binary) + if not os.path.exists(thisfile): + print("Unable to find '%s'" % thisfile) + sys.exit(1) + + basedir = os.path.split(thisfile)[0] + tapset = os.path.join(basedir, "..", "share", "systemtap", "tapset") + return os.path.realpath(tapset) + + +def tapset_env(tapset_dir): + tenv = copy.copy(os.environ) + tenv["SYSTEMTAP_TAPSET"] = tapset_dir + return tenv + +def cmd_run(args): + prefix = probe_prefix(args.binary) + tapsets = tapset_dir(args.binary) + + if args.verbose: + print("Using tapset dir '%s' for binary '%s'" % (tapsets, args.binary)) + + probes = [] + for probe in args.probes: + probes.append("probe %s.%s {}" % (prefix, probe)) + if len(probes) == 0: + print("At least one probe pattern must be specified") + sys.exit(1) + + script = " ".join(probes) + if args.verbose: + print("Compiling script '%s'" % script) + script = """probe begin { print("Running script, <Ctrl>-c to quit\\n") } """ + script + + # We request an 8MB buffer, since the stap default 1MB buffer + # can be easily overflowed by frequently firing QEMU traces + stapargs = ["stap", "-s", "8"] + if args.pid is not None: + stapargs.extend(["-x", args.pid]) + stapargs.extend(["-e", script]) + subprocess.call(stapargs, env=tapset_env(tapsets)) + + +def cmd_list(args): + tapsets = tapset_dir(args.binary) + + if args.verbose: + print("Using tapset dir '%s' for binary '%s'" % (tapsets, args.binary)) + + def print_probes(verbose, name): + prefix = probe_prefix(args.binary) + offset = len(prefix) + 1 + script = prefix + "." + name + + if verbose: + print("Listing probes with name '%s'" % script) + proc = subprocess.Popen(["stap", "-l", script], + stdout=subprocess.PIPE, env=tapset_env(tapsets)) + out, err = proc.communicate() + if proc.returncode != 0: + print("No probes found, are the tapsets installed in %s" % tapset_dir(args.binary)) + sys.exit(1) + + for line in out.splitlines(): + if line.startswith(prefix): + print("%s" % line[offset:]) + + if len(args.probes) == 0: + print_probes(args.verbose, "*") + else: + for probe in args.probes: + print_probes(args.verbose, probe) + + +def main(): + parser = argparse.ArgumentParser(description="QEMU SystemTap trace tool") + parser.add_argument("-v", "--verbose", help="Print verbose progress info", + action='store_true') + + subparser = parser.add_subparsers(help="commands") + subparser.required = True + subparser.dest = "command" + + runparser = subparser.add_parser("run", help="Run a trace session", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" + +To watch all trace points on the qemu-system-x86_64 binary: + + %(argv0)s run qemu-system-x86_64 + +To only watch the trace points matching the qio* and qcrypto* patterns + + %(argv0)s run qemu-system-x86_64 'qio*' 'qcrypto*' +""" % {"argv0": sys.argv[0]}) + runparser.set_defaults(func=cmd_run) + runparser.add_argument("--pid", "-p", dest="pid", + help="Restrict tracing to a specific process ID") + runparser.add_argument("binary", help="QEMU system or user emulator binary") + runparser.add_argument("probes", help="Probe names or wildcards", + nargs=argparse.REMAINDER) + + listparser = subparser.add_parser("list", help="List probe points", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" + +To list all trace points on the qemu-system-x86_64 binary: + + %(argv0)s list qemu-system-x86_64 + +To only list the trace points matching the qio* and qcrypto* patterns + + %(argv0)s list qemu-system-x86_64 'qio*' 'qcrypto*' +""" % {"argv0": sys.argv[0]}) + listparser.set_defaults(func=cmd_list) + listparser.add_argument("binary", help="QEMU system or user emulator binary") + listparser.add_argument("probes", help="Probe names or wildcards", + nargs=argparse.REMAINDER) + + args = parser.parse_args() + + args.func(args) + sys.exit(0) + +if __name__ == '__main__': + main() diff --git a/scripts/qemu-trace-stap.texi b/scripts/qemu-trace-stap.texi new file mode 100644 index 0000000000..07bb9eb94e --- /dev/null +++ b/scripts/qemu-trace-stap.texi @@ -0,0 +1,140 @@ +@example +@c man begin SYNOPSIS +@command{qemu-trace-stap} @var{GLOBAL-OPTIONS} @var{COMMAND} @var{COMMAND-OPTIONS} @var{ARGS...} +@c man end +@end example + +@c man begin DESCRIPTION + +The @command{qemu-trace-stap} program facilitates tracing of the execution +of QEMU emulators using SystemTap. + +It is required to have the SystemTap runtime environment installed to use +this program, since it is a wrapper around execution of the @command{stap} +program. + +@c man end + +@c man begin OPTIONS + +The following global options may be used regardless of which command +is executed: + +@table @option +@item @var{--verbose}, @var{-v} + +Display verbose information about command execution. + +@end table + +The following commands are valid: + +@table @option + +@item @var{list} @var{BINARY} @var{PATTERN...} + +List all the probe names provided by @var{BINARY} that match +@var{PATTERN}. + +If @var{BINARY} is not an absolute path, it will be located by searching +the directories listed in the @code{$PATH} environment variable. + +@var{PATTERN} is a plain string that is used to filter the results of +this command. It may optionally contain a @code{*} wildcard to facilitate +matching multiple probes without listing each one explicitly. Multiple +@var{PATTERN} arguments may be given, causing listing of probes that match +any of the listed names. If no @var{PATTERN} is given, the all possible +probes will be listed. + +For example, to list all probes available in the @command{qemu-system-x86_64} +binary: + +@example +$ qemu-trace-stap list qemu-system-x86_64 +@end example + +To filter the list to only cover probes related to QEMU's cryptographic +subsystem, in a binary outside @code{$PATH} + +@example +$ qemu-trace-stap list /opt/qemu/4.0.0/bin/qemu-system-x86_64 'qcrypto*' +@end example + + +@item @var{run} @var{OPTIONS} @var{BINARY} @var{PATTERN...} + +Run a trace session, printing formatted output any time a process that is +executing @var{BINARY} triggers a probe matching @var{PATTERN}. + +If @var{BINARY} is not an absolute path, it will be located by searching +the directories listed in the @code{$PATH} environment variable. + +@var{PATTERN} is a plain string that matches a probe name shown by the +@var{list} command. It may optionally contain a @code{*} wildcard to +facilitate matching multiple probes without listing each one explicitly. +Multiple @var{PATTERN} arguments may be given, causing all matching probes +to be monitored. At least one @var{PATTERN} is required, since stap is not +capable of tracing all known QEMU probes concurrently without overflowing +its trace buffer. + +Invocation of this command does not need to be synchronized with +invocation of the QEMU process(es). It will match probes on all +existing running processes and all future launched processes, +unless told to only monitor a specific process. + +Valid command specific options are: + +@table @option +@item @var{--pid=PID}, @var{-p PID} + +Restrict the tracing session so that it only triggers for the process +identified by @code{PID}. + +@end table + +For example, to monitor all processes executing @command{qemu-system-x86_64} +as found on $PATH, displaying all I/O related probes: + +@example +$ qemu-trace-stap run qemu-system-x86_64 'qio*' +@end example + +To monitor only the QEMU process with PID 1732 + +@example +$ qemu-trace-stap run --pid=1732 qemu-system-x86_64 'qio*' +@end example + +To monitor QEMU processes running an alternative binary outside of +@code{$PATH}, displaying verbose information about setup of the +tracing environment: + +@example +$ qemu-trace-stap -v run /opt/qemu/4.0.0/qemu-system-x86_64 'qio*' +@end example + +@end table + +@c man end + +@ignore + +@setfilename qemu-trace-stap +@settitle QEMU SystemTap trace tool + +@c man begin LICENSE + +Copyright (C) 2019 Red Hat, Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +@c man end + +@c man begin SEEALSO +qemu(1), stap(1) +@c man end + +@end ignore diff --git a/scripts/qtest.py b/scripts/qtest.py index adf1fe3f26..afac3fe900 100644 --- a/scripts/qtest.py +++ b/scripts/qtest.py @@ -31,6 +31,7 @@ class QEMUQtestProtocol(object): """ self._address = address self._sock = self._get_sock() + self._sockfile = None if server: self._sock.bind(self._address) self._sock.listen(1) @@ -49,6 +50,7 @@ class QEMUQtestProtocol(object): @raise socket.error on socket connection errors """ self._sock.connect(self._address) + self._sockfile = self._sock.makefile() def accept(self): """ @@ -57,6 +59,7 @@ class QEMUQtestProtocol(object): @raise socket.error on socket connection errors """ self._sock, _ = self._sock.accept() + self._sockfile = self._sock.makefile() def cmd(self, qtest_cmd): """ @@ -65,9 +68,12 @@ class QEMUQtestProtocol(object): @param qtest_cmd: qtest command text to be sent """ self._sock.sendall((qtest_cmd + "\n").encode('utf-8')) + resp = self._sockfile.readline() + return resp def close(self): self._sock.close() + self._sockfile.close() def settimeout(self, timeout): self._sock.settimeout(timeout) diff --git a/scripts/render_block_graph.py b/scripts/render_block_graph.py new file mode 100755 index 0000000000..ed7e581b4f --- /dev/null +++ b/scripts/render_block_graph.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python +# +# Render Qemu Block Graph +# +# Copyright (c) 2018 Virtuozzo International GmbH. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import os +import sys +import subprocess +import json +from graphviz import Digraph +from qemu import MonitorResponseError + + +def perm(arr): + s = 'w' if 'write' in arr else '_' + s += 'r' if 'consistent-read' in arr else '_' + s += 'u' if 'write-unchanged' in arr else '_' + s += 'g' if 'graph-mod' in arr else '_' + s += 's' if 'resize' in arr else '_' + return s + + +def render_block_graph(qmp, filename, format='png'): + ''' + Render graph in text (dot) representation into "@filename" and + representation in @format into "@filename.@format" + ''' + + bds_nodes = qmp.command('query-named-block-nodes') + bds_nodes = {n['node-name']: n for n in bds_nodes} + + job_nodes = qmp.command('query-block-jobs') + job_nodes = {n['device']: n for n in job_nodes} + + block_graph = qmp.command('x-debug-query-block-graph') + + graph = Digraph(comment='Block Nodes Graph') + graph.format = format + graph.node('permission symbols:\l' + ' w - Write\l' + ' r - consistent-Read\l' + ' u - write - Unchanged\l' + ' g - Graph-mod\l' + ' s - reSize\l' + 'edge label scheme:\l' + ' <child type>\l' + ' <perm>\l' + ' <shared_perm>\l', shape='none') + + for n in block_graph['nodes']: + if n['type'] == 'block-driver': + info = bds_nodes[n['name']] + label = n['name'] + ' [' + info['drv'] + ']' + if info['drv'] == 'file': + label += '\n' + os.path.basename(info['file']) + shape = 'ellipse' + elif n['type'] == 'block-job': + info = job_nodes[n['name']] + label = info['type'] + ' job (' + n['name'] + ')' + shape = 'box' + else: + assert n['type'] == 'block-backend' + label = n['name'] if n['name'] else 'unnamed blk' + shape = 'box' + + graph.node(str(n['id']), label, shape=shape) + + for e in block_graph['edges']: + label = '%s\l%s\l%s\l' % (e['name'], perm(e['perm']), + perm(e['shared-perm'])) + graph.edge(str(e['parent']), str(e['child']), label=label) + + graph.render(filename) + + +class LibvirtGuest(): + def __init__(self, name): + self.name = name + + def command(self, cmd): + # only supports qmp commands without parameters + m = {'execute': cmd} + ar = ['virsh', 'qemu-monitor-command', self.name, json.dumps(m)] + + reply = json.loads(subprocess.check_output(ar)) + + if 'error' in reply: + raise MonitorResponseError(reply) + + return reply['return'] + + +if __name__ == '__main__': + obj = sys.argv[1] + out = sys.argv[2] + + if os.path.exists(obj): + # assume unix socket + qmp = QEMUMonitorProtocol(obj) + qmp.connect() + else: + # assume libvirt guest name + qmp = LibvirtGuest(obj) + + render_block_graph(qmp, out) diff --git a/scripts/tracetool/__init__.py b/scripts/tracetool/__init__.py index 0e3c9e146c..6fca674936 100644 --- a/scripts/tracetool/__init__.py +++ b/scripts/tracetool/__init__.py @@ -274,6 +274,10 @@ class Event(object): props = groups["props"].split() fmt = groups["fmt"] fmt_trans = groups["fmt_trans"] + if fmt.find("%m") != -1 or fmt_trans.find("%m") != -1: + raise ValueError("Event format '%m' is forbidden, pass the error " + "as an explicit trace argument") + if len(fmt_trans) > 0: fmt = [fmt_trans, fmt] args = Arguments.build(groups["args"]) @@ -350,6 +354,8 @@ def read_events(fobj, fname): events = [] for lineno, line in enumerate(fobj, 1): + if line[-1] != '\n': + raise ValueError("%s does not end with a new line" % fname) if not line.strip(): continue if line.lstrip().startswith('#'): diff --git a/scripts/tracetool/format/log_stap.py b/scripts/tracetool/format/log_stap.py new file mode 100644 index 0000000000..3ccbc09d61 --- /dev/null +++ b/scripts/tracetool/format/log_stap.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Generate .stp file that printfs log messages (DTrace with SystemTAP only). +""" + +__author__ = "Daniel P. Berrange <berrange@redhat.com>" +__copyright__ = "Copyright (C) 2014-2019, Red Hat, Inc." +__license__ = "GPL version 2 or (at your option) any later version" + +__maintainer__ = "Daniel Berrange" +__email__ = "berrange@redhat.com" + +import re + +from tracetool import out +from tracetool.backend.dtrace import binary, probeprefix +from tracetool.backend.simple import is_string +from tracetool.format.stap import stap_escape + +def global_var_name(name): + return probeprefix().replace(".", "_") + "_" + name + +STATE_SKIP = 0 +STATE_LITERAL = 1 +STATE_MACRO = 2 + +def c_macro_to_format(macro): + if macro.startswith("PRI"): + return macro[3] + + if macro == "TARGET_FMT_plx": + return "%016x" + + raise Exception("Unhandled macro '%s'" % macro) + +def c_fmt_to_stap(fmt): + state = 0 + bits = [] + literal = "" + macro = "" + escape = 0; + for i in range(len(fmt)): + if fmt[i] == '\\': + if escape: + escape = 0 + else: + escape = 1 + if state != STATE_LITERAL: + raise Exception("Unexpected escape outside string literal") + literal = literal + fmt[i] + elif fmt[i] == '"' and not escape: + if state == STATE_LITERAL: + state = STATE_SKIP + bits.append(literal) + literal = "" + else: + if state == STATE_MACRO: + bits.append(c_macro_to_format(macro)) + state = STATE_LITERAL + elif fmt[i] == ' ' or fmt[i] == '\t': + if state == STATE_MACRO: + bits.append(c_macro_to_format(macro)) + macro = "" + state = STATE_SKIP + elif state == STATE_LITERAL: + literal = literal + fmt[i] + else: + escape = 0 + if state == STATE_SKIP: + state = STATE_MACRO + + if state == STATE_LITERAL: + literal = literal + fmt[i] + else: + macro = macro + fmt[i] + + if state == STATE_MACRO: + bits.append(c_macro_to_format(macro)) + elif state == STATE_LITERAL: + bits.append(literal) + + fmt = re.sub("%(\d*)z(x|u|d)", "%\\1\\2", "".join(bits)) + return fmt + +def generate(events, backend, group): + out('/* This file is autogenerated by tracetool, do not edit. */', + '') + + for event_id, e in enumerate(events): + if 'disable' in e.properties: + continue + + out('probe %(probeprefix)s.log.%(name)s = %(probeprefix)s.%(name)s ?', + '{', + probeprefix=probeprefix(), + name=e.name) + + # Get references to userspace strings + for type_, name in e.args: + name = stap_escape(name) + if is_string(type_): + out(' try {', + ' arg%(name)s_str = %(name)s ? ' + + 'user_string_n(%(name)s, 512) : "<null>"', + ' } catch {}', + name=name) + + # Determine systemtap's view of variable names + fields = ["pid()", "gettimeofday_ns()"] + for type_, name in e.args: + name = stap_escape(name) + if is_string(type_): + fields.append("arg" + name + "_str") + else: + fields.append(name) + + # Emit the entire record in a single SystemTap printf() + arg_str = ', '.join(arg for arg in fields) + fmt_str = "%d@%d " + e.name + " " + c_fmt_to_stap(e.fmt) + "\\n" + out(' printf("%(fmt_str)s", %(arg_str)s)', + fmt_str=fmt_str, arg_str=arg_str) + + out('}') + + out() |