pw_log_tokenized: Log metadata updates
- Add an optional line number to the 32-bit log tokenized payload.
- Update default payload field widths.
Log level: 3 bits
Line number: 11 bits (up to 2047, 0 if larger)
Flags: 2 bits (implementation defined)
Module token: 16 bits
- Use the maximum log level to indicate that a log is an assert. This
preserves an additional bit which can be used for the flags or line
number.
- Store data as key-value pairs in the format string.
- Reorganize pw_log_tokenized tests and add C tests.
Change-Id: I1daa2e6ce40038f96857caeb38976cf48f620dc3
Requires: pigweed-internal:12920
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/47861
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Wyatt Hepler <hepler@google.com>
diff --git a/pw_assert_log/assert_log.cc b/pw_assert_log/assert_log.cc
index 9d8b9ba..278fa49 100644
--- a/pw_assert_log/assert_log.cc
+++ b/pw_assert_log/assert_log.cc
@@ -19,11 +19,11 @@
extern "C" void pw_assert_HandleFailure(void) {
#if PW_ASSERT_ENABLE_DEBUG
PW_LOG(PW_LOG_LEVEL_CRITICAL,
- PW_LOG_ASSERT_FAILED_FLAG,
+ PW_LOG_DEFAULT_FLAGS,
"Crash: PW_ASSERT() or PW_DASSERT() failure");
#else
PW_LOG(PW_LOG_LEVEL_CRITICAL,
- PW_LOG_ASSERT_FAILED_FLAG,
+ PW_LOG_DEFAULT_FLAGS,
"Crash: PW_ASSERT() failure. Note: PW_DASSERT disabled");
#endif // PW_ASSERT_ENABLE_DEBUG
PW_UNREACHABLE;
diff --git a/pw_assert_log/public/pw_assert_log/assert_log.h b/pw_assert_log/public/pw_assert_log/assert_log.h
index d26d339..9745827 100644
--- a/pw_assert_log/public/pw_assert_log/assert_log.h
+++ b/pw_assert_log/public/pw_assert_log/assert_log.h
@@ -19,16 +19,13 @@
#include "pw_preprocessor/compiler.h"
#include "pw_preprocessor/util.h"
-// Use the highest available log flag to indicate an assert failure.
-#define PW_LOG_ASSERT_FAILED_FLAG (1u << (PW_LOG_FLAG_BITS - 1u))
-
// Die with a message with several attributes included. This crash frontend
// funnels everything into the logger, which must then handle the true crash
// behaviour.
#define PW_HANDLE_CRASH(message, ...) \
do { \
- PW_LOG(PW_LOG_LEVEL_CRITICAL, \
- PW_LOG_ASSERT_FAILED_FLAG, \
+ PW_LOG(PW_LOG_LEVEL_FATAL, \
+ PW_LOG_DEFAULT_FLAGS, \
__FILE__ ":%d: Crash: " message, \
__LINE__, \
__VA_ARGS__); \
@@ -40,8 +37,8 @@
// log, then crashing/rebooting the device.
#define PW_HANDLE_ASSERT_FAILURE(condition_string, message, ...) \
do { \
- PW_LOG(PW_LOG_LEVEL_CRITICAL, \
- PW_LOG_ASSERT_FAILED_FLAG, \
+ PW_LOG(PW_LOG_LEVEL_FATAL, \
+ PW_LOG_DEFAULT_FLAGS, \
__FILE__ ":%d: Check failed: " condition_string ". " message, \
__LINE__, \
__VA_ARGS__); \
@@ -64,8 +61,8 @@
type_fmt, \
message, ...) \
do { \
- PW_LOG(PW_LOG_LEVEL_CRITICAL, \
- PW_LOG_ASSERT_FAILED_FLAG, \
+ PW_LOG(PW_LOG_LEVEL_FATAL, \
+ PW_LOG_DEFAULT_FLAGS, \
__FILE__ ":%d: Check failed: " \
arg_a_str " (=" type_fmt ") " \
comparison_op_str " " \
diff --git a/pw_log/public/pw_log/levels.h b/pw_log/public/pw_log/levels.h
index a70abc3..8702837 100644
--- a/pw_log/public/pw_log/levels.h
+++ b/pw_log/public/pw_log/levels.h
@@ -24,6 +24,8 @@
#define PW_LOG_LEVEL_ERROR 4
#define PW_LOG_LEVEL_CRITICAL 5
+#define PW_LOG_LEVEL_FATAL 7
+
// Number of bits required to represent the log level
#define PW_LOG_LEVEL_BITS 3
#define PW_LOG_LEVEL_BITMASK 7 // 0b111
diff --git a/pw_log_tokenized/BUILD b/pw_log_tokenized/BUILD
index 41b0747..80b841d 100644
--- a/pw_log_tokenized/BUILD
+++ b/pw_log_tokenized/BUILD
@@ -27,6 +27,7 @@
hdrs = [
"public/pw_log_tokenized/config.h",
"public/pw_log_tokenized/log_tokenized.h",
+ "public/pw_log_tokenized/metadata.h",
"public_overrides/pw_log_backend/log_backend.h",
],
includes = [
@@ -59,9 +60,22 @@
)
pw_cc_test(
- name = "test",
+ name = "log_tokenized_test",
srcs = [
- "test.cc",
+ "log_tokenized_test.cc",
+ "log_tokenized_test_c.c",
+ "pw_log_tokenized_private/test_utils.h",
+ ],
+ deps = [
+ ":headers",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
+ name = "metadata_test",
+ srcs = [
+ "metadata_test.cc",
],
deps = [
":headers",
diff --git a/pw_log_tokenized/BUILD.gn b/pw_log_tokenized/BUILD.gn
index a89054d..2109673 100644
--- a/pw_log_tokenized/BUILD.gn
+++ b/pw_log_tokenized/BUILD.gn
@@ -45,18 +45,31 @@
":public_include_path",
]
public_deps = [
- "$dir_pw_log:facade",
+ ":config",
+ ":metadata",
"$dir_pw_tokenizer:global_handler_with_payload.facade",
- dir_pw_preprocessor,
- pw_log_tokenized_CONFIG,
]
public = [
- "public/pw_log_tokenized/config.h",
"public/pw_log_tokenized/log_tokenized.h",
"public_overrides/pw_log_backend/log_backend.h",
]
}
+pw_source_set("metadata") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ ":config" ]
+ public = [ "public/pw_log_tokenized/metadata.h" ]
+}
+
+pw_source_set("config") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ "$dir_pw_log:facade",
+ pw_log_tokenized_CONFIG,
+ ]
+ public = [ "public/pw_log_tokenized/config.h" ]
+}
+
# The log backend deps that might cause circular dependencies, since
# pw_log is so ubiquitous. These deps are kept separate so they can be
# depended on from elsewhere.
@@ -82,12 +95,27 @@
}
pw_test_group("tests") {
- tests = [ ":log_tokenized_test" ]
+ tests = [
+ ":log_tokenized_test",
+ ":metadata_test",
+ ]
}
pw_test("log_tokenized_test") {
- sources = [ "test.cc" ]
- deps = [ ":pw_log_tokenized" ]
+ sources = [
+ "log_tokenized_test.cc",
+ "log_tokenized_test_c.c",
+ "pw_log_tokenized_private/test_utils.h",
+ ]
+ deps = [
+ ":pw_log_tokenized",
+ dir_pw_preprocessor,
+ ]
+}
+
+pw_test("metadata_test") {
+ sources = [ "metadata_test.cc" ]
+ deps = [ ":metadata" ]
}
pw_doc_group("docs") {
diff --git a/pw_log_tokenized/docs.rst b/pw_log_tokenized/docs.rst
index 0871c6b..1aa6267 100644
--- a/pw_log_tokenized/docs.rst
+++ b/pw_log_tokenized/docs.rst
@@ -39,6 +39,95 @@
See the documentation for :ref:`module-pw_tokenizer` for further details.
+Metadata in the format string
+-----------------------------
+With tokenized logging, the log format string is converted to a 32-bit token.
+Regardless of how long the format string is, it's always represented by a 32-bit
+token. Because of this, metadata can be packed into the tokenized string with
+no cost.
+
+``pw_log_tokenized`` uses a simple key-value format to encode metadata in a
+format string. Each field starts with the ``■`` (U+25A0 "Black Square")
+character, followed by the key name, the ``♦`` (U+2666 "Black Diamond Suit")
+character, and then the value. The string is encoded as UTF-8. Key names are
+comprised of alphanumeric ASCII characters and underscore and start with a
+letter.
+
+.. code-block::
+
+ "■key1♦contents1■key2♦contents2■key3♦contents3"
+
+This format makes the message easily machine parseable and human readable. It is
+extremely unlikely to conflict with log message contents due to the characters
+used.
+
+``pw_log_tokenized`` uses three fields: ``msg``, ``module``, and ``file``.
+Implementations may add other fields, but they will be ignored by the
+``pw_log_tokenized`` tooling.
+
+.. code-block::
+
+ "■msg♦Hyperdrive %d set to %f■module♦engine■file♦propulsion/hyper.cc"
+
+Using key-value pairs allows placing the fields in any order.
+``pw_log_tokenized`` places the message first. This is prefered when tokenizing
+C code because the tokenizer only hashes a fixed number of characters. If the
+file were first, the long path might take most of the hashed characters,
+increasing the odds of a collision with other strings in that file. In C++, all
+characters in the string are hashed, so the order is not important.
+
+Metadata in the tokenizer payload argument
+-------------------------------------------
+``pw_log_tokenized`` packs runtime-accessible metadata into a 32-bit integer
+which is passed as the "payload" argument for ``pw_log_tokenizer``'s global
+handler with payload facade. Packing this metadata into a single word rather
+than separate arguments reduces the code size significantly.
+
+Four items are packed into the payload argument:
+
+- Log level -- Used for runtime log filtering by level.
+- Line number -- Used to track where a log message originated.
+- Log flags -- Implementation-defined log flags.
+- Tokenized :c:macro:`PW_LOG_MODULE_NAME` -- Used for runtime log filtering by
+ module.
+
+Configuring metadata bit fields
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+The number of bits to use for each metadata field is configurable through macros
+in ``pw_log/config.h``. The field widths must sum to 32 bits. A field with zero
+bits allocated is excluded from the log metadata.
+
+.. c:macro:: PW_LOG_TOKENIZED_LEVEL_BITS
+
+ Bits to allocate for the log level. Defaults to :c:macro:`PW_LOG_LEVEL_BITS`
+ (3).
+
+.. c:macro:: PW_LOG_TOKENIZED_LINE_BITS
+
+ Bits to allocate for the line number. Defaults to 11 (up to line 2047). If the
+ line number is too large to be represented by this field, line is reported as
+ 0.
+
+ Including the line number can slightly increase code size. Without the line
+ number, the log metadata argument is the same for all logs with the same level
+ and flags. With the line number, each metadata value is unique and must be
+ encoded as a separate word in the binary. Systems with extreme space
+ constraints may exclude line numbers by setting this macro to 0.
+
+ It is possible to include line numbers in tokenized log format strings, but
+ that is discouraged because line numbers change whenever a file is edited.
+ Passing the line number with the metadata is a lightweight way to include it.
+
+.. c:macro:: PW_LOG_TOKENIZED_FLAG_BITS
+
+ Bits to use for implementation-defined flags. Defaults to 2.
+
+.. c:macro:: PW_LOG_TOKENIZED_MODULE_BITS
+
+ Bits to use for the tokenized version of :c:macro:`PW_LOG_MODULE_NAME`.
+ Defaults to 16, which gives a ~1% probability of a collision with 37 module
+ names.
+
Using a custom macro
--------------------
Applications may use their own macro instead of
diff --git a/pw_log_tokenized/log_tokenized_test.cc b/pw_log_tokenized/log_tokenized_test.cc
new file mode 100644
index 0000000..e926749
--- /dev/null
+++ b/pw_log_tokenized/log_tokenized_test.cc
@@ -0,0 +1,140 @@
+// 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.
+
+#define PW_LOG_MODULE_NAME "log module name!"
+
+// Configure the module so that the test runs against known values.
+#undef PW_LOG_TOKENIZED_LEVEL_BITS
+#undef PW_LOG_TOKENIZED_MODULE_BITS
+#undef PW_LOG_TOKENIZED_FLAG_BITS
+#undef PW_LOG_TOKENIZED_LINE_BITS
+
+#define PW_LOG_TOKENIZED_LEVEL_BITS 3
+#define PW_LOG_TOKENIZED_MODULE_BITS 16
+#define PW_LOG_TOKENIZED_FLAG_BITS 2
+#define PW_LOG_TOKENIZED_LINE_BITS 11
+
+#include "pw_log_tokenized/log_tokenized.h"
+
+#include "gtest/gtest.h"
+#include "pw_log_tokenized_private/test_utils.h"
+
+namespace pw::log_tokenized {
+namespace {
+
+TEST(LogTokenized, FormatString) {
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(63, 1023, "hello %d", 1);
+ EXPECT_STREQ(last_log.format_string,
+ "■msg♦hello %d■module♦log module name!■file♦" __FILE__);
+}
+
+constexpr uintptr_t kModuleToken =
+ PW_TOKENIZER_STRING_TOKEN(PW_LOG_MODULE_NAME) &
+ ((1u << PW_LOG_TOKENIZED_MODULE_BITS) - 1);
+
+TEST(LogTokenized, LogMetadata_LevelTooLarge_Clamps) {
+ auto check_metadata = [] {
+ Metadata metadata = Metadata(last_log.metadata);
+ EXPECT_EQ(metadata.level(), 7u);
+ EXPECT_EQ(metadata.flags(), 0u);
+ EXPECT_EQ(metadata.module(), kModuleToken);
+ EXPECT_TRUE(metadata.line_number() == 55u || metadata.line_number() == 45u);
+ };
+
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(8, 0, "hello");
+ check_metadata();
+
+ pw_log_tokenized_Test_LogMetadata_LevelTooLarge_Clamps();
+ check_metadata();
+}
+
+TEST(LogTokenized, LogMetadata_TooManyFlags_Truncates) {
+ auto check_metadata = [] {
+ Metadata metadata = Metadata(last_log.metadata);
+ EXPECT_EQ(metadata.level(), 1u);
+ EXPECT_EQ(metadata.flags(), 0b11u);
+ EXPECT_EQ(metadata.module(), kModuleToken);
+ EXPECT_TRUE(metadata.line_number() == 71u || metadata.line_number() == 49u);
+ };
+
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(1, 0xFFFFFFFF, "hello");
+ check_metadata();
+
+ pw_log_tokenized_Test_LogMetadata_TooManyFlags_Truncates();
+ check_metadata();
+}
+
+TEST(LogTokenized, LogMetadata_VariousValues) {
+ auto check_metadata = [] {
+ Metadata metadata = Metadata(last_log.metadata);
+ EXPECT_EQ(metadata.level(), 6u);
+ EXPECT_EQ(metadata.flags(), 3u);
+ EXPECT_EQ(metadata.module(), kModuleToken);
+ EXPECT_EQ(last_log.arg_count, 1u);
+ EXPECT_TRUE(metadata.line_number() == 88u || metadata.line_number() == 53u);
+ };
+
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(6, 3, "hello%s", "?");
+ check_metadata();
+
+ pw_log_tokenized_Test_LogMetadata_LogMetadata_VariousValues();
+ check_metadata();
+}
+
+TEST(LogTokenized, LogMetadata_Zero) {
+ auto check_metadata = [] {
+ Metadata metadata = Metadata(last_log.metadata);
+ EXPECT_EQ(metadata.level(), 0u);
+ EXPECT_EQ(metadata.flags(), 0u);
+ EXPECT_EQ(metadata.module(), kModuleToken);
+ EXPECT_EQ(last_log.arg_count, 0u);
+ EXPECT_TRUE(metadata.line_number() == 106u ||
+ metadata.line_number() == 57u);
+ };
+
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(0, 0, "hello");
+ check_metadata();
+
+ pw_log_tokenized_Test_LogMetadata_LogMetadata_Zero();
+ check_metadata();
+}
+
+TEST(LogTokenized, LogMetadata_MaxValues) {
+#line 2047
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(7, 3, "hello %d", 1);
+
+ Metadata metadata = Metadata(last_log.metadata);
+ EXPECT_EQ(metadata.line_number(), 2047u);
+ EXPECT_EQ(metadata.level(), 7u);
+ EXPECT_EQ(metadata.flags(), 3u);
+ EXPECT_EQ(metadata.module(), kModuleToken);
+ EXPECT_EQ(last_log.arg_count, 1u);
+}
+
+TEST(LogTokenized, LogMetadata_LineNumberTooLarge_IsZero) {
+#line 2048 // At 11 bits, the largest representable line is 2047
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(7, 3, "hello %d", 1);
+ EXPECT_EQ(Metadata(last_log.metadata).line_number(), 0u);
+
+#line 2049
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(7, 3, "hello %d", 1);
+ EXPECT_EQ(Metadata(last_log.metadata).line_number(), 0u);
+
+#line 99999
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(7, 3, "hello %d", 1);
+ EXPECT_EQ(Metadata(last_log.metadata).line_number(), 0u);
+}
+
+} // namespace
+} // namespace pw::log_tokenized
diff --git a/pw_log_tokenized/log_tokenized_test_c.c b/pw_log_tokenized/log_tokenized_test_c.c
new file mode 100644
index 0000000..c0b28b7
--- /dev/null
+++ b/pw_log_tokenized/log_tokenized_test_c.c
@@ -0,0 +1,58 @@
+// 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.
+
+#define PW_LOG_MODULE_NAME "log module name!"
+
+// Configure the module so that the test runs against known values.
+#undef PW_LOG_TOKENIZED_LEVEL_BITS
+#undef PW_LOG_TOKENIZED_MODULE_BITS
+#undef PW_LOG_TOKENIZED_FLAG_BITS
+#undef PW_LOG_TOKENIZED_LINE_BITS
+
+#define PW_LOG_TOKENIZED_LEVEL_BITS 3
+#define PW_LOG_TOKENIZED_MODULE_BITS 16
+#define PW_LOG_TOKENIZED_FLAG_BITS 2
+#define PW_LOG_TOKENIZED_LINE_BITS 11
+
+#include "pw_log_tokenized/log_tokenized.h"
+#include "pw_log_tokenized_private/test_utils.h"
+
+pw_log_tokenized_CapturedLog last_log;
+
+void pw_log_tokenized_CaptureArgs(uintptr_t payload,
+ size_t arg_count,
+ const char* message,
+ ...) {
+ last_log.metadata = payload;
+ last_log.format_string = message;
+ last_log.arg_count = arg_count;
+}
+
+// These functions correspond to tests in log_tokenized_test.cc. The tests call
+// these functions and check the results.
+void pw_log_tokenized_Test_LogMetadata_LevelTooLarge_Clamps(void) {
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(8, 0, "hello");
+}
+
+void pw_log_tokenized_Test_LogMetadata_TooManyFlags_Truncates(void) {
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(1, 0xFFFFFFFF, "hello");
+}
+
+void pw_log_tokenized_Test_LogMetadata_LogMetadata_VariousValues(void) {
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(6, 3, "hello%s", "?");
+}
+
+void pw_log_tokenized_Test_LogMetadata_LogMetadata_Zero(void) {
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(0, 0, "hello");
+}
diff --git a/pw_log_tokenized/metadata_test.cc b/pw_log_tokenized/metadata_test.cc
new file mode 100644
index 0000000..3a45cee
--- /dev/null
+++ b/pw_log_tokenized/metadata_test.cc
@@ -0,0 +1,67 @@
+// 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_log_tokenized/metadata.h"
+
+#include "gtest/gtest.h"
+
+namespace pw::log_tokenized {
+namespace {
+
+TEST(Metadata, NoLineBits) {
+ using NoLineBits = internal::GenericMetadata<6, 0, 10, 16>;
+
+ constexpr NoLineBits test1 = NoLineBits::Set<0, 0, 0>();
+ static_assert(test1.level() == 0);
+ static_assert(test1.module() == 0);
+ static_assert(test1.flags() == 0);
+ static_assert(test1.line_number() == 0);
+
+ constexpr NoLineBits test2 = NoLineBits::Set<3, 2, 1>();
+ static_assert(test2.level() == 3);
+ static_assert(test2.module() == 2);
+ static_assert(test2.flags() == 1);
+ static_assert(test2.line_number() == 0);
+
+ constexpr NoLineBits test3 = NoLineBits::Set<63, 65535, 1023>();
+ static_assert(test3.level() == 63);
+ static_assert(test3.module() == 65535);
+ static_assert(test3.flags() == 1023);
+ static_assert(test3.line_number() == 0);
+}
+
+TEST(Metadata, NoFlagBits) {
+ using NoFlagBits = internal::GenericMetadata<3, 13, 0, 16>;
+
+ constexpr NoFlagBits test1 = NoFlagBits::Set<0, 0, 0, 0>();
+ static_assert(test1.level() == 0);
+ static_assert(test1.module() == 0);
+ static_assert(test1.flags() == 0);
+ static_assert(test1.line_number() == 0);
+
+ constexpr NoFlagBits test2 = NoFlagBits::Set<3, 2, 0, 1>();
+ static_assert(test2.level() == 3);
+ static_assert(test2.module() == 2);
+ static_assert(test2.flags() == 0);
+ static_assert(test2.line_number() == 1);
+
+ constexpr NoFlagBits test3 = NoFlagBits::Set<7, 65535, 0, (1 << 13) - 1>();
+ static_assert(test3.level() == 7);
+ static_assert(test3.module() == 65535);
+ static_assert(test3.flags() == 0);
+ static_assert(test3.line_number() == (1 << 13) - 1);
+}
+
+} // namespace
+} // namespace pw::log_tokenized
diff --git a/pw_log_tokenized/public/pw_log_tokenized/config.h b/pw_log_tokenized/public/pw_log_tokenized/config.h
index 9c92d9f..de33510 100644
--- a/pw_log_tokenized/public/pw_log_tokenized/config.h
+++ b/pw_log_tokenized/public/pw_log_tokenized/config.h
@@ -15,39 +15,63 @@
#include <assert.h>
+#include "pw_log/levels.h"
#include "pw_log/options.h"
-#include "pw_preprocessor/concat.h"
// This macro takes the PW_LOG format string and optionally transforms it. By
-// default, the PW_LOG_MODULE_NAME is prepended to the string if present.
+// default, pw_log_tokenized specifies three fields as key-value pairs.
#ifndef PW_LOG_TOKENIZED_FORMAT_STRING
-#define PW_LOG_TOKENIZED_FORMAT_STRING(string) \
- PW_CONCAT(PW_LOG_TOKENIZED_FMT_, PW_LOG_MODULE_NAME_DEFINED)(string)
+#define _PW_LOG_TOKENIZED_FIELD(name, contents) "■" name "♦" contents
-#define PW_LOG_TOKENIZED_FMT_0(string) string
-#define PW_LOG_TOKENIZED_FMT_1(string) PW_LOG_MODULE_NAME " " string
+#define PW_LOG_TOKENIZED_FORMAT_STRING(string) \
+ _PW_LOG_TOKENIZED_FIELD("msg", string) \
+ _PW_LOG_TOKENIZED_FIELD("module", PW_LOG_MODULE_NAME) \
+ _PW_LOG_TOKENIZED_FIELD("file", __FILE__)
#endif // PW_LOG_TOKENIZED_FORMAT_STRING
-// The log level, module token, and flag bits are packed into the tokenizer's
-// payload argument, which is typically 32 bits. These macros specify the number
-// of bits to use for each field.
+// The log level, line number, flag bits, and module token are packed into the
+// tokenizer's payload argument, which is typically 32 bits. These macros
+// specify the number of bits to use for each field. A field with zero bits is
+// excluded.
+
+// Bits to allocate for the log level.
#ifndef PW_LOG_TOKENIZED_LEVEL_BITS
-#define PW_LOG_TOKENIZED_LEVEL_BITS 6
+#define PW_LOG_TOKENIZED_LEVEL_BITS PW_LOG_LEVEL_BITS
#endif // PW_LOG_TOKENIZED_LEVEL_BITS
+// Bits to allocate for the line number. Defaults to 11 (up to line 2047). If
+// the line number is too large to be represented by this field, line is
+// reported as 0.
+//
+// Including the line number can slightly increase code size. Without the line
+// number, the log metadata argument is the same for all logs with the same
+// level and flags. With the line number, each metadata value is unique and must
+// be encoded as a separate word in the binary. Systems with extreme space
+// constraints may exclude line numbers by setting this macro to 0.
+//
+// It is possible to include line numbers in tokenized log format strings, but
+// that is discouraged because line numbers change whenever a file is edited.
+// Passing the line number with the metadata is a lightweight way to include it.
+#ifndef PW_LOG_TOKENIZED_LINE_BITS
+#define PW_LOG_TOKENIZED_LINE_BITS 11
+#endif // PW_LOG_TOKENIZED_LINE_BITS
+
+// Bits to use for implementation-defined flags.
+#ifndef PW_LOG_TOKENIZED_FLAG_BITS
+#define PW_LOG_TOKENIZED_FLAG_BITS 2
+#endif // PW_LOG_TOKENIZED_FLAG_BITS
+
+// Bits to use for the tokenized version of PW_LOG_MODULE_NAME. Defaults to 16,
+// which gives a ~1% probability of a collision with 37 module names.
#ifndef PW_LOG_TOKENIZED_MODULE_BITS
#define PW_LOG_TOKENIZED_MODULE_BITS 16
#endif // PW_LOG_TOKENIZED_MODULE_BITS
-#ifndef PW_LOG_TOKENIZED_FLAG_BITS
-#define PW_LOG_TOKENIZED_FLAG_BITS 10
-#endif // PW_LOG_TOKENIZED_FLAG_BITS
-
-static_assert((PW_LOG_TOKENIZED_LEVEL_BITS + PW_LOG_TOKENIZED_MODULE_BITS +
- PW_LOG_TOKENIZED_FLAG_BITS) == 32,
- "Log metadata must fit in a 32-bit integer");
+static_assert((PW_LOG_TOKENIZED_LEVEL_BITS + PW_LOG_TOKENIZED_LINE_BITS +
+ PW_LOG_TOKENIZED_FLAG_BITS + PW_LOG_TOKENIZED_MODULE_BITS) == 32,
+ "Log metadata fields must use 32 bits");
// The macro to use to tokenize the log and its arguments. Defaults to
// PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD. Projects may define their own
diff --git a/pw_log_tokenized/public/pw_log_tokenized/log_tokenized.h b/pw_log_tokenized/public/pw_log_tokenized/log_tokenized.h
index 34acb74..42cc614 100644
--- a/pw_log_tokenized/public/pw_log_tokenized/log_tokenized.h
+++ b/pw_log_tokenized/public/pw_log_tokenized/log_tokenized.h
@@ -18,6 +18,11 @@
#include "pw_log_tokenized/config.h"
#include "pw_tokenizer/tokenize_to_global_handler_with_payload.h"
+// TODO(hepler): Remove this include.
+#ifdef __cplusplus
+#include "pw_log_tokenized/metadata.h"
+#endif // __cplusplus
+
// This macro implements PW_LOG using
// PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD or an equivalent alternate macro
// provided by PW_LOG_TOKENIZED_ENCODE_MESSAGE. The log level, module token, and
@@ -45,76 +50,54 @@
#define PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD( \
level, flags, message, ...) \
do { \
- _PW_TOKENIZER_CONST uintptr_t _pw_log_module_token = \
+ _PW_TOKENIZER_CONST uintptr_t _pw_log_tokenized_module_token = \
PW_TOKENIZE_STRING_MASK("pw_log_module_names", \
((1u << PW_LOG_TOKENIZED_MODULE_BITS) - 1u), \
PW_LOG_MODULE_NAME); \
+ const uintptr_t _pw_log_tokenized_level = level; \
PW_LOG_TOKENIZED_ENCODE_MESSAGE( \
- ((uintptr_t)(level) | \
- (_pw_log_module_token << PW_LOG_TOKENIZED_LEVEL_BITS) | \
- ((uintptr_t)(flags) \
- << (PW_LOG_TOKENIZED_LEVEL_BITS + PW_LOG_TOKENIZED_MODULE_BITS))), \
+ (_PW_LOG_TOKENIZED_LEVEL(_pw_log_tokenized_level) | \
+ _PW_LOG_TOKENIZED_MODULE(_pw_log_tokenized_module_token) | \
+ _PW_LOG_TOKENIZED_FLAGS(flags) | _PW_LOG_TOKENIZED_LINE()), \
PW_LOG_TOKENIZED_FORMAT_STRING(message), \
__VA_ARGS__); \
} while (0)
-#ifdef __cplusplus
+// If the level field is present, clamp it to the maximum value.
+#if PW_LOG_TOKENIZED_LEVEL_BITS == 0
+#define _PW_LOG_TOKENIZED_LEVEL(value) ((uintptr_t)0)
+#else
+#define _PW_LOG_TOKENIZED_LEVEL(value) \
+ (value < ((uintptr_t)1 << PW_LOG_TOKENIZED_LEVEL_BITS) \
+ ? value \
+ : ((uintptr_t)1 << PW_LOG_TOKENIZED_LEVEL_BITS) - 1)
+#endif // PW_LOG_TOKENIZED_LEVEL_BITS
-namespace pw {
-namespace log_tokenized {
-namespace internal {
+// If the line number field is present, shift it to its position. Set it to zero
+// if the line number is too large for PW_LOG_TOKENIZED_LINE_BITS.
+#if PW_LOG_TOKENIZED_LINE_BITS == 0
+#define _PW_LOG_TOKENIZED_LINE() ((uintptr_t)0)
+#else
+#define _PW_LOG_TOKENIZED_LINE() \
+ ((uintptr_t)(__LINE__ < (1 << PW_LOG_TOKENIZED_LINE_BITS) ? __LINE__ : 0) \
+ << PW_LOG_TOKENIZED_LEVEL_BITS)
+#endif // PW_LOG_TOKENIZED_LINE_BITS
-// This class, which is aliased to pw::log_tokenized::Metadata below, is used to
-// access the log metadata packed into the tokenizer's payload argument.
-template <unsigned kLevelBits,
- unsigned kModuleBits,
- unsigned kFlagBits,
- typename T = uintptr_t>
-class GenericMetadata {
- public:
- template <T log_level, T module, T flags>
- static constexpr GenericMetadata Set() {
- static_assert(log_level < (1 << kLevelBits), "The level is too large!");
- static_assert(module < (1 << kModuleBits), "The module is too large!");
- static_assert(flags < (1 << kFlagBits), "The flags are too large!");
+// If the flags field is present, mask it and shift it to its position.
+#if PW_LOG_TOKENIZED_FLAG_BITS == 0
+#define _PW_LOG_TOKENIZED_FLAGS(value) ((uintptr_t)0)
+#else
+#define _PW_LOG_TOKENIZED_FLAGS(value) \
+ (((uintptr_t)(value) & (((uintptr_t)1 << PW_LOG_TOKENIZED_FLAG_BITS) - 1)) \
+ << (PW_LOG_TOKENIZED_LEVEL_BITS + PW_LOG_TOKENIZED_LINE_BITS))
+#endif // PW_LOG_TOKENIZED_FLAG_BITS
- return GenericMetadata(log_level | (module << kLevelBits) |
- (flags << (kModuleBits + kLevelBits)));
- }
-
- constexpr GenericMetadata(T value) : bits_(value) {}
-
- // The log level of this message.
- constexpr T level() const { return bits_ & Mask<kLevelBits>(); }
-
- // The 16 bit tokenized version of the module name (PW_LOG_MODULE_NAME).
- constexpr T module() const {
- return (bits_ >> kLevelBits) & Mask<kModuleBits>();
- }
-
- // The flags provided to the log call.
- constexpr T flags() const {
- return (bits_ >> (kLevelBits + kModuleBits)) & Mask<kFlagBits>();
- }
-
- private:
- template <int bits>
- static constexpr T Mask() {
- return (T(1) << bits) - 1;
- }
-
- T bits_;
-
- static_assert(kLevelBits + kModuleBits + kFlagBits <= sizeof(bits_) * 8);
-};
-
-} // namespace internal
-
-using Metadata = internal::GenericMetadata<PW_LOG_TOKENIZED_LEVEL_BITS,
- PW_LOG_TOKENIZED_MODULE_BITS,
- PW_LOG_TOKENIZED_FLAG_BITS>;
-
-} // namespace log_tokenized
-} // namespace pw
-
-#endif // __cpluplus
+// If the module field is present, shift it to its position.
+#if PW_LOG_TOKENIZED_MODULE_BITS == 0
+#define _PW_LOG_TOKENIZED_MODULE(value) ((uintptr_t)0)
+#else
+#define _PW_LOG_TOKENIZED_MODULE(value) \
+ ((uintptr_t)(value) << ((PW_LOG_TOKENIZED_LEVEL_BITS + \
+ PW_LOG_TOKENIZED_LINE_BITS + \
+ PW_LOG_TOKENIZED_FLAG_BITS)))
+#endif // PW_LOG_TOKENIZED_MODULE_BITS
diff --git a/pw_log_tokenized/public/pw_log_tokenized/metadata.h b/pw_log_tokenized/public/pw_log_tokenized/metadata.h
new file mode 100644
index 0000000..86e6364
--- /dev/null
+++ b/pw_log_tokenized/public/pw_log_tokenized/metadata.h
@@ -0,0 +1,99 @@
+// 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 <cstdint>
+
+#include "pw_log_tokenized/config.h"
+
+namespace pw {
+namespace log_tokenized {
+namespace internal {
+
+// Internal class for managing the metadata bit fields.
+template <typename T, unsigned kBits, unsigned kShift>
+struct BitField {
+ public:
+ static constexpr T Get(T value) { return (value >> kShift) & kMask; }
+ static constexpr T Shift(T value) {
+ return (value <= kMask ? value : T(0)) << kShift;
+ }
+
+ private:
+ static constexpr T kMask = (T(1) << kBits) - 1;
+};
+
+template <typename T, unsigned kShift>
+class BitField<T, 0, kShift> {
+ public:
+ static constexpr T Get(T) { return 0; }
+ static constexpr T Shift(T) { return 0; }
+};
+
+// This class, which is aliased to pw::log_tokenized::Metadata below, is used to
+// access the log metadata packed into the tokenizer's payload argument.
+template <unsigned kLevelBits,
+ unsigned kLineBits,
+ unsigned kFlagBits,
+ unsigned kModuleBits,
+ typename T = uintptr_t>
+class GenericMetadata {
+ public:
+ template <T log_level = 0, T module = 0, T flags = 0, T line = 0>
+ static constexpr GenericMetadata Set() {
+ static_assert(log_level < (1 << kLevelBits), "The level is too large!");
+ static_assert(line < (1 << kLineBits), "The line number is too large!");
+ static_assert(flags < (1 << kFlagBits), "The flags are too large!");
+ static_assert(module < (1 << kModuleBits), "The module is too large!");
+
+ return GenericMetadata(Level::Shift(log_level) | Module::Shift(module) |
+ Flags::Shift(flags) | Line::Shift(line));
+ }
+
+ constexpr GenericMetadata(T value) : bits_(value) {}
+
+ // The log level of this message.
+ constexpr T level() const { return Level::Get(bits_); }
+
+ // The line number of the log call. The first line in a file is 1. If the line
+ // number is 0, it was too large to be stored.
+ constexpr T line_number() const { return Line::Get(bits_); }
+
+ // The flags provided to the log call.
+ constexpr T flags() const { return Flags::Get(bits_); }
+
+ // The 16 bit tokenized version of the module name (PW_LOG_MODULE_NAME).
+ constexpr T module() const { return Module::Get(bits_); }
+
+ private:
+ using Level = BitField<T, kLevelBits, 0>;
+ using Line = BitField<T, kLineBits, kLevelBits>;
+ using Flags = BitField<T, kFlagBits, kLevelBits + kLineBits>;
+ using Module = BitField<T, kModuleBits, kLevelBits + kLineBits + kFlagBits>;
+
+ T bits_;
+
+ static_assert(kLevelBits + kLineBits + kFlagBits + kModuleBits <=
+ sizeof(bits_) * 8);
+};
+
+} // namespace internal
+
+using Metadata = internal::GenericMetadata<PW_LOG_TOKENIZED_LEVEL_BITS,
+ PW_LOG_TOKENIZED_LINE_BITS,
+ PW_LOG_TOKENIZED_FLAG_BITS,
+ PW_LOG_TOKENIZED_MODULE_BITS>;
+
+} // namespace log_tokenized
+} // namespace pw
diff --git a/pw_log_tokenized/pw_log_tokenized_private/test_utils.h b/pw_log_tokenized/pw_log_tokenized_private/test_utils.h
new file mode 100644
index 0000000..dce3a3d
--- /dev/null
+++ b/pw_log_tokenized/pw_log_tokenized_private/test_utils.h
@@ -0,0 +1,52 @@
+// 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 <stddef.h>
+#include <stdint.h>
+
+#include "pw_log_tokenized/log_tokenized.h"
+#include "pw_preprocessor/arguments.h"
+#include "pw_preprocessor/compiler.h"
+#include "pw_preprocessor/util.h"
+
+// Create a fake version of the tokenization macro.
+#undef PW_LOG_TOKENIZED_ENCODE_MESSAGE
+#define PW_LOG_TOKENIZED_ENCODE_MESSAGE(payload, message, ...) \
+ pw_log_tokenized_CaptureArgs(payload, \
+ PW_MACRO_ARG_COUNT(__VA_ARGS__), \
+ message PW_COMMA_ARGS(__VA_ARGS__))
+
+PW_EXTERN_C_START
+
+typedef struct {
+ uintptr_t metadata;
+ const char* format_string;
+ size_t arg_count;
+} pw_log_tokenized_CapturedLog;
+
+extern pw_log_tokenized_CapturedLog last_log;
+
+void pw_log_tokenized_CaptureArgs(uintptr_t payload,
+ size_t arg_count,
+ const char* message,
+ ...) PW_PRINTF_FORMAT(3, 4);
+
+// C versions of tests.
+void pw_log_tokenized_Test_LogMetadata_LevelTooLarge_Clamps(void);
+void pw_log_tokenized_Test_LogMetadata_TooManyFlags_Truncates(void);
+void pw_log_tokenized_Test_LogMetadata_LogMetadata_VariousValues(void);
+void pw_log_tokenized_Test_LogMetadata_LogMetadata_Zero(void);
+
+PW_EXTERN_C_END
diff --git a/pw_log_tokenized/test.cc b/pw_log_tokenized/test.cc
deleted file mode 100644
index 7a843f4..0000000
--- a/pw_log_tokenized/test.cc
+++ /dev/null
@@ -1,100 +0,0 @@
-// 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.
-
-#define PW_LOG_MODULE_NAME "This is the log module name!"
-
-// Create a fake version of the tokenization macro.
-#undef PW_LOG_TOKENIZED_ENCODE_MESSAGE
-#define PW_LOG_TOKENIZED_ENCODE_MESSAGE(payload, message, ...) \
- CaptureTokenizerArgs(payload, \
- PW_MACRO_ARG_COUNT(__VA_ARGS__), \
- message PW_COMMA_ARGS(__VA_ARGS__))
-
-#include <cstring>
-#include <string_view>
-#include <tuple>
-
-#include "gtest/gtest.h"
-#include "pw_log_tokenized/log_tokenized.h"
-#include "pw_preprocessor/arguments.h"
-#include "pw_preprocessor/compiler.h"
-
-namespace pw::log_tokenized {
-namespace {
-
-struct {
- Metadata metadata = Metadata(0);
- const char* format_string = "";
- size_t arg_count = 0;
-} last_log{};
-
-void CaptureTokenizerArgs(pw_tokenizer_Payload payload,
- size_t arg_count,
- const char* message,
- ...) PW_PRINTF_FORMAT(3, 4);
-
-void CaptureTokenizerArgs(pw_tokenizer_Payload payload,
- size_t arg_count,
- const char* message,
- ...) {
- last_log.metadata = payload;
- last_log.format_string = message;
- last_log.arg_count = arg_count;
-}
-
-constexpr uintptr_t kModuleToken =
- PW_TOKENIZER_STRING_TOKEN(PW_LOG_MODULE_NAME) &
- ((1u << PW_LOG_TOKENIZED_MODULE_BITS) - 1);
-
-constexpr Metadata test1 = Metadata::Set<0, 0, 0>();
-static_assert(test1.level() == 0);
-static_assert(test1.module() == 0);
-static_assert(test1.flags() == 0);
-
-constexpr Metadata test2 = Metadata::Set<3, 2, 1>();
-static_assert(test2.level() == 3);
-static_assert(test2.module() == 2);
-static_assert(test2.flags() == 1);
-
-constexpr Metadata test3 = Metadata::Set<63, 65535, 1023>();
-static_assert(test3.level() == 63);
-static_assert(test3.module() == 65535);
-static_assert(test3.flags() == 1023);
-
-TEST(LogTokenized, LogMetadata_Zero) {
- PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(0, 0, "hello");
- EXPECT_EQ(last_log.metadata.level(), 0u);
- EXPECT_EQ(last_log.metadata.flags(), 0u);
- EXPECT_EQ(last_log.metadata.module(), kModuleToken);
- EXPECT_EQ(last_log.arg_count, 0u);
-}
-
-TEST(LogTokenized, LogMetadata_DifferentValues) {
- PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(55, 36, "hello%s", "?");
- EXPECT_EQ(last_log.metadata.level(), 55u);
- EXPECT_EQ(last_log.metadata.flags(), 36u);
- EXPECT_EQ(last_log.metadata.module(), kModuleToken);
- EXPECT_EQ(last_log.arg_count, 1u);
-}
-
-TEST(LogTokenized, LogMetadata_MaxValues) {
- PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(63, 1023, "hello %d", 1);
- EXPECT_EQ(last_log.metadata.level(), 63u);
- EXPECT_EQ(last_log.metadata.flags(), 1023u);
- EXPECT_EQ(last_log.metadata.module(), kModuleToken);
- EXPECT_EQ(last_log.arg_count, 1u);
-}
-
-} // namespace
-} // namespace pw::log_tokenized
diff --git a/pw_tokenizer/docs.rst b/pw_tokenizer/docs.rst
index 8967f12..168bc4a 100644
--- a/pw_tokenizer/docs.rst
+++ b/pw_tokenizer/docs.rst
@@ -975,8 +975,9 @@
.. attention::
Do not encode line numbers in tokenized strings. This results in a huge
number of lines being added to the database, since every time code moves,
- new strings are tokenized. If line numbers are desired in a tokenized
- string, add a ``"%d"`` to the string and pass ``__LINE__`` as an argument.
+ new strings are tokenized. If :ref:`module-pw_log_tokenized` is used, line
+ numbers are encoded in the log metadata. Line numbers may also be included by
+ by adding ``"%d"`` to the format string and passing ``__LINE__``.
Database management
-------------------