pw_trace: Add basic RPCs to get trace
Add a basic RPC service to trace which currently provides 3 RPCs:
- Enable(bool), Turns tracing on or off
- IsEnabled, Returns if tracing is currently on or off
- GetTraceData, Streams the encoded trace data.
This CL also adds a python script which uses these RPCs to retrieve
and decode trace data from a connected device.
Change-Id: Iaceb3fa87017939a17512738101af70bf335504a
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/31880
Reviewed-by: Paul Mathieu <paulmathieu@google.com>
Commit-Queue: Rob Oliver <rgoliver@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index 0c1ebdb..ae068a3 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -201,6 +201,7 @@
"$dir_pw_trace:trace_example_basic",
"$dir_pw_trace_tokenized:trace_tokenized_example_basic",
"$dir_pw_trace_tokenized:trace_tokenized_example_filter",
+ "$dir_pw_trace_tokenized:trace_tokenized_example_rpc",
"$dir_pw_trace_tokenized:trace_tokenized_example_trigger",
]
}
diff --git a/pw_trace_tokenized/BUILD b/pw_trace_tokenized/BUILD
index 4c3175f..1944d71 100644
--- a/pw_trace_tokenized/BUILD
+++ b/pw_trace_tokenized/BUILD
@@ -61,6 +61,24 @@
)
pw_cc_library(
+ name = "trace_rpc_service",
+ hdrs = [
+ "public/pw_trace_tokenized/trace_rpc_service_nanopb.h",
+ ],
+ includes = [
+ "public",
+ ],
+ srcs = [
+ "trace_rpc_service_nanopb.cc",
+ ],
+ deps = [
+ "//pw_log",
+ "//pw_trace",
+ "//pw_trace_tokenized_buffer",
+ ],
+)
+
+pw_cc_library(
name = "trace_buffer_headers",
hdrs = [
"public/pw_trace_tokenized/trace_buffer.h",
@@ -205,3 +223,17 @@
],
srcs = [ "example/filter.cc" ]
)
+
+pw_cc_library(
+ name = "trace_tokenized_example_rpc",
+ deps = [
+ ":pw_trace_rpc_service",
+ "//dir_pw_rpc:server",
+ "//dir_pw_rpc:system_server",
+ "//pw_log",
+ "//pw_hdlc",
+ "//dir_pw_trace",
+ "//dir_pw_trace:pw_trace_sample_app",
+ ],
+ srcs = [ "example/rpc.cc" ]
+)
\ No newline at end of file
diff --git a/pw_trace_tokenized/BUILD.gn b/pw_trace_tokenized/BUILD.gn
index c031e89..8468e8a 100644
--- a/pw_trace_tokenized/BUILD.gn
+++ b/pw_trace_tokenized/BUILD.gn
@@ -15,7 +15,10 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/module_config.gni")
+import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_protobuf_compiler/proto.gni")
+import("$dir_pw_third_party/nanopb/nanopb.gni")
import("$dir_pw_unit_test/test.gni")
declare_args() {
@@ -85,6 +88,25 @@
defines = [ "PW_TRACE_BUFFER_SIZE_BYTES=${pw_trace_tokenized_BUFFER_SIZE}" ]
}
+pw_proto_library("trace_rpc_service_proto") {
+ sources = [ "pw_trace_protos/trace_rpc.proto" ]
+ inputs = [ "pw_trace_protos/trace_rpc.options" ]
+}
+
+pw_source_set("trace_rpc_service") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ ":trace_rpc_service_proto.nanopb_rpc" ]
+ deps = [
+ ":tokenized_trace_buffer",
+ "$dir_pw_log",
+ "$dir_pw_trace",
+ ]
+ sources = [
+ "public/pw_trace_tokenized/trace_rpc_service_nanopb.h",
+ "trace_rpc_service_nanopb.cc",
+ ]
+}
+
pw_source_set("tokenized_trace_buffer") {
deps = [ ":pw_trace_tokenized_core" ]
public_deps = [
@@ -209,3 +231,21 @@
]
sources = [ "example/filter.cc" ]
}
+
+if (dir_pw_third_party_nanopb == "") {
+ group("trace_tokenized_example_rpc") {
+ }
+} else {
+ pw_executable("trace_tokenized_example_rpc") {
+ sources = [ "example/rpc.cc" ]
+ deps = [
+ ":trace_rpc_service",
+ "$dir_pw_hdlc",
+ "$dir_pw_log",
+ "$dir_pw_rpc:server",
+ "$dir_pw_rpc/system_server",
+ "$dir_pw_trace",
+ "$dir_pw_trace:trace_sample_app",
+ ]
+ }
+}
diff --git a/pw_trace_tokenized/example/rpc.cc b/pw_trace_tokenized/example/rpc.cc
new file mode 100644
index 0000000..8126d6a
--- /dev/null
+++ b/pw_trace_tokenized/example/rpc.cc
@@ -0,0 +1,66 @@
+// Copyright 2021 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+//==============================================================================
+/*
+BUILD
+ninja -C out
+host_clang_debug/obj/pw_trace_tokenized/bin/trace_tokenized_example_rpc
+
+RUN
+.out/host_clang_debug/obj/pw_trace_tokenized/bin/trace_tokenized_example_rpc
+
+DECODE
+python pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py
+ -s localhost:33000
+ -o trace.json
+ -t
+ out/host_clang_debug/obj/pw_trace_tokenized/bin/trace_tokenized_example_rpc
+ pw_trace_tokenized/pw_trace_protos/trace_rpc.proto
+
+VIEW
+In chrome navigate to chrome://tracing, and load the trace.json file.
+*/
+#include <thread>
+
+#include "pw_log/log.h"
+#include "pw_rpc/server.h"
+#include "pw_rpc_system_server/rpc_server.h"
+#include "pw_trace/example/sample_app.h"
+#include "pw_trace/trace.h"
+#include "pw_trace_tokenized/trace_rpc_service_nanopb.h"
+
+namespace {
+
+pw::trace::TraceService trace_service;
+
+void RpcThread() {
+ pw::rpc::system_server::Init();
+
+ // Set up the server and start processing data.
+ pw::rpc::system_server::Server().RegisterService(trace_service);
+ pw::rpc::system_server::Start();
+}
+
+} // namespace
+
+int main() {
+ std::thread rpc_thread(RpcThread);
+
+ // Enable tracing.
+ PW_TRACE_SET_ENABLED(true);
+
+ PW_LOG_INFO("Running basic trace example...\n");
+ RunTraceSampleApp();
+ return 0;
+}
\ No newline at end of file
diff --git a/pw_trace_tokenized/public/pw_trace_tokenized/trace_rpc_service_nanopb.h b/pw_trace_tokenized/public/pw_trace_tokenized/trace_rpc_service_nanopb.h
new file mode 100644
index 0000000..cc326c1
--- /dev/null
+++ b/pw_trace_tokenized/public/pw_trace_tokenized/trace_rpc_service_nanopb.h
@@ -0,0 +1,35 @@
+// Copyright 2021 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_trace_protos/trace_rpc.rpc.pb.h"
+
+namespace pw::trace {
+
+class TraceService final : public generated::TraceService<TraceService> {
+ public:
+ pw::Status Enable(ServerContext&,
+ const pw_trace_TraceEnableMessage& request,
+ pw_trace_TraceEnableMessage& response);
+
+ pw::Status IsEnabled(ServerContext&,
+ const pw_trace_Empty& request,
+ pw_trace_TraceEnableMessage& response);
+
+ void GetTraceData(ServerContext&,
+ const pw_trace_Empty& request,
+ ServerWriter<pw_trace_TraceDataMessage>& writer);
+};
+
+} // namespace pw::trace
diff --git a/pw_trace_tokenized/pw_trace_protos/trace_rpc.options b/pw_trace_tokenized/pw_trace_protos/trace_rpc.options
new file mode 100644
index 0000000..37508ac
--- /dev/null
+++ b/pw_trace_tokenized/pw_trace_protos/trace_rpc.options
@@ -0,0 +1,15 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+pw.trace.TraceDataMessage.data max_size:64
diff --git a/pw_trace_tokenized/pw_trace_protos/trace_rpc.proto b/pw_trace_tokenized/pw_trace_protos/trace_rpc.proto
new file mode 100644
index 0000000..39971e7
--- /dev/null
+++ b/pw_trace_tokenized/pw_trace_protos/trace_rpc.proto
@@ -0,0 +1,32 @@
+// Copyright 2021 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+syntax = "proto3";
+
+package pw.trace;
+
+service TraceService {
+ rpc Enable(TraceEnableMessage) returns (TraceEnableMessage) {}
+ rpc IsEnabled(Empty) returns (TraceEnableMessage) {}
+ rpc GetTraceData(Empty) returns (stream TraceDataMessage) {}
+}
+
+message Empty {}
+
+message TraceEnableMessage {
+ bool enable = 1;
+}
+
+message TraceDataMessage {
+ bytes data = 1;
+}
diff --git a/pw_trace_tokenized/py/BUILD.gn b/pw_trace_tokenized/py/BUILD.gn
index 00b85f8..ef75ba6 100644
--- a/pw_trace_tokenized/py/BUILD.gn
+++ b/pw_trace_tokenized/py/BUILD.gn
@@ -20,6 +20,7 @@
setup = [ "setup.py" ]
sources = [
"pw_trace_tokenized/__init__.py",
+ "pw_trace_tokenized/get_trace.py",
"pw_trace_tokenized/trace_tokenized.py",
]
python_deps = [ "$dir_pw_tokenizer/py" ]
diff --git a/pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py b/pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py
new file mode 100755
index 0000000..988f62b
--- /dev/null
+++ b/pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+# Copyright 2021 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+r"""
+Generates json trace files viewable using chrome://tracing using RPCs from a
+connected HdlcRpcClient.
+
+Example usage:
+python pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py -s localhost:33000
+ -o trace.json
+ -t out/host_clang_debug/obj/pw_trace_tokenized/bin/trace_tokenized_example_rpc
+ pw_trace_tokenized/pw_trace_protos/trace_rpc.proto
+"""
+import argparse
+import logging
+import glob
+from pathlib import Path
+import sys
+from typing import Collection, Iterable, Iterator
+import serial # type: ignore
+from pw_tokenizer import database
+from pw_trace import trace
+from pw_hdlc.rpc import HdlcRpcClient, default_channels
+from pw_hdlc.rpc_console import SocketClientImpl
+from pw_trace_tokenized import trace_tokenized
+
+_LOG = logging.getLogger('pw_trace_tokenizer')
+
+PW_RPC_MAX_PACKET_SIZE = 256
+SOCKET_SERVER = 'localhost'
+SOCKET_PORT = 33000
+MKFIFO_MODE = 0o666
+
+
+def _expand_globs(globs: Iterable[str]) -> Iterator[Path]:
+ for pattern in globs:
+ for file in glob.glob(pattern, recursive=True):
+ yield Path(file)
+
+
+def get_hdlc_rpc_client(device: str, baudrate: int,
+ proto_globs: Collection[str], socket_addr: str,
+ **kwargs):
+ """Get the HdlcRpcClient based on arguments."""
+ del kwargs # ignore
+ if not proto_globs:
+ proto_globs = ['**/*.proto']
+
+ protos = list(_expand_globs(proto_globs))
+
+ if not protos:
+ _LOG.critical('No .proto files were found with %s',
+ ', '.join(proto_globs))
+ _LOG.critical('At least one .proto file is required')
+ return 1
+
+ _LOG.debug('Found %d .proto files found with %s', len(protos),
+ ', '.join(proto_globs))
+
+ # TODO(rgoliver): When pw has a generalized transport for RPC this should
+ # use it so it isn't specific to HDLC
+ if socket_addr is None:
+ serial_device = serial.Serial(device, baudrate, timeout=1)
+ read = lambda: serial_device.read(8192)
+ write = serial_device.write
+ else:
+ try:
+ socket_device = SocketClientImpl(socket_addr)
+ read = socket_device.read
+ write = socket_device.write
+ except ValueError:
+ _LOG.exception('Failed to initialize socket at %s', socket_addr)
+ return 1
+
+ return HdlcRpcClient(read, protos, default_channels(write))
+
+
+def get_trace_data_from_device(client):
+ """ Get the trace data using RPC from a Client"""
+ data = b''
+ result = \
+ client.client.channel(1).rpcs.pw.trace.TraceService.GetTraceData().get()
+ for streamed_data in result:
+ data = data + bytes([len(streamed_data.data)])
+ data = data + streamed_data.data
+ _LOG.debug(''.join(format(x, '02x') for x in streamed_data.data))
+ return data
+
+
+def _parse_args():
+ """Parse and return command line arguments."""
+
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+ group = parser.add_mutually_exclusive_group(required=True)
+ group.add_argument('-d', '--device', help='the serial port to use')
+ parser.add_argument('-b',
+ '--baudrate',
+ type=int,
+ default=115200,
+ help='the baud rate to use')
+ group.add_argument('-s',
+ '--socket-addr',
+ type=str,
+ help='use socket to connect to server, type default for\
+ localhost:33000, or manually input the server address:port')
+ parser.add_argument('-o',
+ '--trace_output',
+ dest='trace_output_file',
+ help=('The json file to which to write the output.'))
+ parser.add_argument(
+ '-t',
+ '--trace_token_database',
+ help='Databases (ELF, binary, or CSV) to use to lookup trace tokens.')
+ parser.add_argument('proto_globs',
+ nargs='+',
+ help='glob pattern for .proto files')
+
+ return parser.parse_args()
+
+
+def _main(args):
+ token_database = \
+ database.load_token_database(args.trace_token_database, domain="trace")
+ _LOG.info(database.database_summary(token_database))
+ client = get_hdlc_rpc_client(**vars(args))
+ data = get_trace_data_from_device(client)
+ events = trace_tokenized.get_trace_events([token_database], data)
+ json_lines = trace.generate_trace_json(events)
+ trace_tokenized.save_trace_file(json_lines, args.trace_output_file)
+
+
+if __name__ == '__main__':
+ if sys.version_info[0] < 3:
+ sys.exit('ERROR: The detokenizer command line tools require Python 3.')
+ _main(_parse_args())
diff --git a/pw_trace_tokenized/py/pw_trace_tokenized/trace_tokenized.py b/pw_trace_tokenized/py/pw_trace_tokenized/trace_tokenized.py
index 3e88a8a..fd16b3c 100755
--- a/pw_trace_tokenized/py/pw_trace_tokenized/trace_tokenized.py
+++ b/pw_trace_tokenized/py/pw_trace_tokenized/trace_tokenized.py
@@ -107,6 +107,7 @@
def parse_trace_event(buffer, db, last_time, ticks_per_second=1000):
+ """Parse a single trace event from bytes"""
us_per_tick = 1000000 / ticks_per_second
idx = 0
# Read token
@@ -116,6 +117,8 @@
# Decode token
if len(db.token_to_entries[token]) == 0:
_LOG.error("token not found: %08x", token)
+ return None
+
token_string = str(db.token_to_entries[token][0])
# Read time
@@ -138,31 +141,52 @@
return create_trace_event(token_string, timestamp_us, trace_id, data)
-def get_trace_events_from_file(databases, input_file_name):
+def get_trace_events(databases, raw_trace_data):
"""Handles the decoding traces."""
db = tokens.Database.merged(*databases)
last_timestamp = 0
events = []
- with open(input_file_name, "rb") as input_file:
- bytes_read = input_file.read()
- idx = 0
+ idx = 0
- while idx + 1 < len(bytes_read):
- # Read size
- size = int(bytes_read[idx])
- if idx + size > len(bytes_read):
- _LOG.error("incomplete file")
- break
+ while idx + 1 < len(raw_trace_data):
+ # Read size
+ size = int(raw_trace_data[idx])
+ if idx + size > len(raw_trace_data):
+ _LOG.error("incomplete file")
+ break
- event = parse_trace_event(bytes_read[idx + 1:idx + 1 + size], db,
- last_timestamp)
+ event = parse_trace_event(raw_trace_data[idx + 1:idx + 1 + size], db,
+ last_timestamp)
+ if event:
last_timestamp = event.timestamp_us
events.append(event)
- idx = idx + size + 1
+ idx = idx + size + 1
return events
+def get_trace_data_from_file(input_file_name):
+ """Handles the decoding traces."""
+ with open(input_file_name, "rb") as input_file:
+ return input_file.read()
+ return None
+
+
+def save_trace_file(trace_lines, file_name):
+ """Handles generating the trace file."""
+ with open(file_name, 'w') as output_file:
+ output_file.write("[")
+ for line in trace_lines:
+ output_file.write("%s,\n" % line)
+ output_file.write("{}]")
+
+
+def get_trace_events_from_file(databases, input_file_name):
+ """Get trace events from a file."""
+ raw_trace_data = get_trace_data_from_file(input_file_name)
+ return get_trace_events(databases, raw_trace_data)
+
+
def _parse_args():
"""Parse and return command line arguments."""
@@ -190,10 +214,7 @@
def _main(args):
events = get_trace_events_from_file(args.databases, args.input_file)
json_lines = trace.generate_trace_json(events)
-
- with open(args.output_file, 'w') as output_file:
- for line in json_lines:
- output_file.write("%s,\n" % line)
+ save_trace_file(json_lines, args.output_file)
if __name__ == '__main__':
diff --git a/pw_trace_tokenized/trace_rpc_service_nanopb.cc b/pw_trace_tokenized/trace_rpc_service_nanopb.cc
new file mode 100644
index 0000000..a6893da
--- /dev/null
+++ b/pw_trace_tokenized/trace_rpc_service_nanopb.cc
@@ -0,0 +1,63 @@
+// Copyright 2021 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+//==============================================================================
+
+#include "pw_trace_tokenized/trace_rpc_service_nanopb.h"
+
+#include "pw_log/log.h"
+#include "pw_preprocessor/util.h"
+#include "pw_trace_tokenized/trace_buffer.h"
+#include "pw_trace_tokenized/trace_tokenized.h"
+
+namespace pw::trace {
+
+pw::Status TraceService::Enable(ServerContext&,
+ const pw_trace_TraceEnableMessage& request,
+ pw_trace_TraceEnableMessage& response) {
+ TokenizedTrace::Instance().Enable(request.enable);
+ response.enable = TokenizedTrace::Instance().IsEnabled();
+ return PW_STATUS_OK;
+}
+
+pw::Status TraceService::IsEnabled(ServerContext&,
+ const pw_trace_Empty&,
+ pw_trace_TraceEnableMessage& response) {
+ response.enable = TokenizedTrace::Instance().IsEnabled();
+ return PW_STATUS_OK;
+}
+
+void TraceService::GetTraceData(
+ ServerContext&,
+ const pw_trace_Empty&,
+ ServerWriter<pw_trace_TraceDataMessage>& writer) {
+ pw_trace_TraceDataMessage buffer = pw_trace_TraceDataMessage_init_default;
+ size_t size = 0;
+ pw::ring_buffer::PrefixedEntryRingBuffer* trace_buffer =
+ pw::trace::GetBuffer();
+
+ while (trace_buffer->PeekFront(
+ std::as_writable_bytes(std::span(buffer.data.bytes)), &size) !=
+ pw::Status::OutOfRange()) {
+ trace_buffer->PopFront();
+ buffer.data.size = size;
+ pw::Status status = writer.Write(buffer);
+ if (!status.ok()) {
+ PW_LOG_ERROR("Error sending trace; abandoning trace dump. Error: %s",
+ status.str());
+ break;
+ }
+ }
+ writer.Finish();
+}
+} // namespace pw::trace
\ No newline at end of file