babeltrace 2.1.2: sink.ctf.fs does not deduplicate identical clocks

"MOESSBAUER, Felix via lttng-dev" <[email protected]>
Newsgroups org.lttng.lists.lttng-dev
Message-ID <[email protected]>
Hi,

while porting the bt2-ftrace-to-ctf [1] plugin to babeltrace 2.1, I
noticed that the sink.ctf.fs creates invalid clock definitions if
multiple stream-classes share the same clock-class. In this case, the
clock definition is added multiple times to the CTF 2 metadata,
resulting in the following assertion on read-back:

ERROR:    [Babeltrace CLI] (babeltrace2.c:2705)
  Command-line error: retcode=1
CAUSED BY [Source auto-discovery] (autodisc/autodisc.c:493)
  babeltrace.support-info query failed.
CAUSED BY [libbabeltrace2] (lib/graph/query-executor.c:233)
  Component class's "query" method failed: query-exec-
addr=0x55cba00c12e0, cc-addr=0x55cba00b4100, cc-type=SOURCE, cc-
name="fs", cc-partial-descr="Read CTF traces from
  the file sy", cc-is-frozen=0, cc-so-handle-addr=0x55cba00b1930, cc-
so-handle-path="<...>/build/babeltrace-plugin-ctf.so",
  object="babeltrace.support-info", params-addr=0x55cba00cd470, params-
type=MAP, params-element-count=2, log-level=WARNING
CAUSED BY ['source.ctf.fs'] (plugins/ctf/common/src/metadata/json/ctf-
2-metadata-stream-parser.cpp:130)
  [1:1 @ 8512 bytes] Invalid fragment #12.
CAUSED BY ['source.ctf.fs'] (plugins/ctf/common/src/metadata/json/ctf-
2-metadata-stream-parser.cpp:367)
  [1:1 @ 17024 bytes] Duplicate clock class fragment with ID `local`.

When running with MIP=0, the CTF 1.8 data does not have this issue (or
the reader ignores the duplicated definitions).

[1] https://github.com/siemens/bt2-ftrace-to-ctf

Attached you will find an AI generated reproducer using just the
babeltrace2 Python bindings.

Best regards,
Felix Moessbauer

-- 
Siemens AG
Linux Expert Center
Friedrich-Ludwig-Bauer-Str. 3
85748 Garching, Germany
repro.py (text/x-python3, 4.5 KB)
#!/usr/bin/env python3
#
# Minimal, self-contained reproducer for a babeltrace2 CTF-2 sink bug.
#
# A single trace that contains two data stream classes sharing ONE clock class
# is written to CTF 2 by sink.ctf.fs. The sink emits one `clock-class` metadata
# fragment PER stream class (both with the same id, here "the_clock") instead
# of emitting the shared clock class only once. Reading the resulting trace
# back then fails with:
#
#   Duplicate clock class fragment with ID `the_clock`.
#
# This depends only on babeltrace2 and its bundled `ctf`/`utils` plugins; no
# external trace files or third-party plugins are needed.
#
# Usage:
#   python3 repro.py <output-dir>
#
# The script writes the trace in a child process (so sink.ctf.fs flushes the
# metadata file on component teardown), then reads it back in the parent.
import os
import subprocess
import sys

import bt2


class TheIter(bt2._UserMessageIterator):
    def __init__(self, config, port):
        stream = port.user_data
        self._msgs = [
            self._create_stream_beginning_message(stream),
            self._create_stream_end_message(stream),
        ]
        self._i = 0

    def __next__(self):
        if self._i >= len(self._msgs):
            raise StopIteration
        msg = self._msgs[self._i]
        self._i += 1
        return msg


@bt2.plugin_component_class
class TheSource(bt2._UserSourceComponent, message_iterator_class=TheIter):
    """One trace, two stream classes, ONE shared clock class."""

    def __init__(self, config, params, obj):
        tc = self._create_trace_class(assigns_automatic_stream_class_id=False)
        cc = self._create_clock_class(frequency=1000000000, name="the_clock")
        sc0 = tc.create_stream_class(
            id=0,
            default_clock_class=cc,
            supports_packets=True,
            assigns_automatic_stream_id=False,
        )
        sc1 = tc.create_stream_class(
            id=1,
            default_clock_class=cc,
            supports_packets=True,
            assigns_automatic_stream_id=False,
        )
        trace = tc()
        self._add_output_port("out0", trace.create_stream(sc0, id=0))
        self._add_output_port("out1", trace.create_stream(sc1, id=1))


def write_trace(out_dir):
    ctf = bt2.find_plugin("ctf")
    utils = bt2.find_plugin("utils")
    sink_cls = ctf.sink_component_classes["fs"]

    # CTF 2 requires MIP 1. The ctf.fs sink has a single input port, so mux
    # the two source ports into it.
    graph = bt2.Graph(mip_version=1)
    src = graph.add_component(TheSource, "src")
    muxer = graph.add_component(utils.filter_component_classes["muxer"], "mux")
    sink = graph.add_component(
        sink_cls, "sink", params={"path": out_dir, "ctf-version": "2"}
    )
    out_ports = list(src.output_ports.values())
    graph.connect_ports(out_ports[0], list(muxer.input_ports.values())[0])
    graph.connect_ports(out_ports[1], list(muxer.input_ports.values())[1])
    graph.connect_ports(
        list(muxer.output_ports.values())[0],
        list(sink.input_ports.values())[0],
    )
    graph.run()


def read_trace(trace_dir):
    for _ in bt2.TraceCollectionMessageIterator(trace_dir):
        pass


def main():
    if len(sys.argv) < 2:
        print("usage: repro.py <output-dir>", file=sys.stderr)
        return 2

    # Internal entry points, invoked as child processes.
    if sys.argv[1] == "--write":
        write_trace(sys.argv[2])
        return 0
    if sys.argv[1] == "--read":
        read_trace(sys.argv[2])
        return 0

    out_dir = sys.argv[1]
    trace_dir = os.path.join(out_dir, "trace")

    # Write in a child process so the sink flushes the metadata on teardown.
    subprocess.run([sys.executable, __file__, "--write", out_dir], check=True)
    print("wrote CTF-2 trace to", trace_dir)

    with open(os.path.join(trace_dir, "metadata"), "rb") as f:
        n_clock = f.read().count(b'"type":"clock-class"')
    print("clock-class fragments written to metadata:", n_clock)

    # Read back with the CLI for a clear, unwrapped error message.
    proc = subprocess.run(
        ["babeltrace2", trace_dir],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.PIPE,
        text=True,
    )
    if proc.returncode != 0:
        print("\nREAD-BACK FAILED (bug reproduced):", file=sys.stderr)
        for line in proc.stderr.splitlines():
            if "Duplicate clock class" in line or "Invalid fragment" in line:
                print("  " + line.split("] ", 1)[-1], file=sys.stderr)
        return 1

    print("read-back OK (bug NOT reproduced)")
    return 0


if __name__ == "__main__":
    sys.exit(main())
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.