Skip to content

Latest commit

 

History

History
436 lines (271 loc) · 23.8 KB

README.md

File metadata and controls

436 lines (271 loc) · 23.8 KB

Diagnosticism

Simple diagnostics utilities for C (and C++) — part of the cross-language Diagnosticism family.

Language Language License GitHub release CI Last Commit

Table of Contents

Introduction

Diagnosticism is a library providing miscellaneous discrete and simple diagnostics facilities to supplement what is available in the standard library. It is implemented in several languages, providing enhancements that are necessary (and possible). For example, Diagnosticism.Python can provide the trace() function that can capture the callstack information to issue into a diagnostic log statement.

In C and C++, which offer only modest built-in diagnostic support, the facilities are aimed at lightweight, low-overhead utilities that integrate cleanly with existing code. For example, the DoomGram component records duration events into an order-of-magnitude histogram and can summarise many thousands of timings in a single 12-character strip — useful for logging cumulative execution costs in long-running performance-sensitive applications. (See the Components section below for more on this.) The diagnosticism_trace() macro provides a simple call-site trace facility, writing file, line, function, and a printf-style message to a chosen stream; see examples/tracing/.

Other facilities include environment-variable helpers (diagnosticism_getenv(), diagnosticism_getenv_atoi()), compact duration formatting (diagnosticism_nanoseconds_to_string() and the C++ wrappers diagnosticism::nanoseconds_to_string()), version-string formatting (diagnosticism_calc_version_string(), diagnosticism_calc_version_string_16(), and the C++ wrappers diagnosticism::calc_version_string() / diagnosticism::calc_version_string_16()), and a small core API for library initialisation and version discovery. Further facilities will be added over time (and will be listed in the Components section below).

Installation

Detailed instructions — via CMake — are provided in the accompanying INSTALL.md file.

CMake consumer projects

After installing Diagnosticism (see INSTALL.md), consumer CMake projects may link against the library as follows:

cmake_minimum_required(VERSION 3.20)
project(myapp C)

find_package(Diagnosticism REQUIRED)

add_executable(myapp main.c)
target_link_libraries(myapp PRIVATE Diagnosticism::core)

Ensure CMAKE_PREFIX_PATH includes the install prefix (for example /usr/local or the path passed to cmake --install).

API documentation (Doxygen)

HTML API reference for the public headers may be generated locally:

./generate_doxygen.sh

Open _build/doxygen/html/index.html in a browser (or <build-dir>/doxygen/html/index.html when SIS_CMAKE_BUILD_DIR is set). Requires Doxygen on PATH.

Components

The C library is organised by header under include/diagnosticism/. Include <diagnosticism/diagnosticism.h> for the core API, and the component headers as required. Each component (and selected later additions) is tagged with a since badge for the Diagnosticism version in which it was introduced; badge colour is constant for a given MAJOR.MINOR (patch releases share the series colour: since 0.1.0, since 0.2.0, since 0.3.0, since 0.4.0).

C API / core library

Constants

The following version constants are defined in <diagnosticism/diagnosticism.h>:

  • DIAGNOSTICISM_VER_MAJOR, DIAGNOSTICISM_VER_MINOR, DIAGNOSTICISM_VER_PATCH - the major, minor, and patch components of the library version;
  • DIAGNOSTICISM_VER_RELSTATUS - release-status discriminator in the low byte of DIAGNOSTICISM_VER (0xFF = full release);
  • DIAGNOSTICISM_VER - a composite 32-bit encoding of the library version;

Types

The following types are defined in the public C headers:

  • diag_uint32_t - an unsigned 32-bit integer type used by the core API (<diagnosticism/diagnosticism.h>);
  • diagnosticism_doomgram_t since 0.1.0 - a Decimal Order-Of-Magnitude histoGram structure that records duration values efficiently in the orders of magnitude 1ns+, 10ns+, 100ns+, 1µs+, ..., 10s+, 100s+ (<diagnosticism/doomgram.h>). This is a C port of the equivalent stlsoft::doomgram class from the STLSoft libraries;

Functions

The following functions comprise the public C API:

Abort (<diagnosticism/abort.h>) since 0.3.0:

  • diagnosticism_abort() - writes a program-name-qualified message to a stream and then aborts;
  • diagnosticism_abortf() - as diagnosticism_abort(), with a printf-style formatted message;

Core API (<diagnosticism/diagnosticism.h>):

  • diagnosticism_api_version() - obtains the Diagnosticism version (at time of compilation); may be called without having initialised the API;
  • diagnosticism_api_init() - initialises the Diagnosticism API;
  • diagnosticism_api_uninit() - uninitialises the Diagnosticism API;

DoomGram (<diagnosticism/doomgram.h>) since 0.1.0:

  • diagnosticism_doomgram_push_event_time_ns() - records an event of the given duration in nanoseconds;
  • diagnosticism_doomgram_push_event_time_us() - records an event of the given duration in microseconds;
  • diagnosticism_doomgram_push_event_time_ms() - records an event of the given duration in milliseconds;
  • diagnosticism_doomgram_push_event_time_s() - records an event of the given duration in seconds;
  • diagnosticism_doomgram_clear() since 0.2.1 - resets all fields of a doomgram to their initial state;
  • diagnosticism_doomgram_try_get_total_event_time_ns() - attempts to obtain the cumulative event time (in nanoseconds), which may fail if overflow has occurred;
  • diagnosticism_doomgram_try_get_min_event_time_ns() - attempts to obtain the minimum recorded event time (in nanoseconds);
  • diagnosticism_doomgram_try_get_max_event_time_ns() - attempts to obtain the maximum recorded event time (in nanoseconds);
  • diagnosticism_doomgram_to_strip_12() - writes a terse 12-character histogram strip summarising the order-of-magnitude distribution; useful for logging cumulative execution costs in long-running performance-sensitive applications. Each character encodes the event count in one magnitude bucket, using _ for zero, a-z for increasing count ranges, and * when a bucket's count exceeds the representable range;

Environment variables (<diagnosticism/ev.h>) since 0.2.0:

  • diagnosticism_getenv() - obtains the value of a named environment variable, or a caller-supplied default when the variable is not set (in which case errno is set to ENOENT);
  • diagnosticism_getenv_atoi() - obtains the value of a named environment variable interpreted as an integer, or a caller-supplied default when the variable is not set (in which case errno is set to ENOENT); returns 0 when the value cannot be interpreted as an integer;

Nanosecond duration strings (<diagnosticism/nanoseconds_to_string.h>) since 0.4.0:

  • diagnosticism_nanoseconds_to_string() - formats a nanosecond count as a compact human-readable duration string into a caller-supplied UTF-8 buffer, adapting the unit (ns, µs, ms, s) and decimal precision to keep roughly three significant digits; zero is always "0s" (no sign); optional DIAGNOSTICISM_NS_TO_STRING_F_PLUS requests a leading + on positive values; optional DIAGNOSTICISM_NS_TO_STRING_F_ASCII_ONLY emits the microsecond unit as "us" instead of "µs". Default behaviour matches Diagnosticism.Python / Diagnosticism.Rust nanoseconds_to_string(). For example:
#include <diagnosticism/nanoseconds_to_string.h>

char    buf[DIAGNOSTICISM_NS_TO_STRING_MIN_CCH];
size_t  n;

diagnosticism_nanoseconds_to_string(sizeof(buf), buf, 123456789, DIAGNOSTICISM_NS_TO_STRING_F_NONE, &n);
/* buf == "123.4ms" */

diagnosticism_nanoseconds_to_string(sizeof(buf), buf, 6789, DIAGNOSTICISM_NS_TO_STRING_F_NONE, &n);
/* buf == "6.789µs" */

diagnosticism_nanoseconds_to_string(sizeof(buf), buf, 6789, DIAGNOSTICISM_NS_TO_STRING_F_ASCII_ONLY, &n);
/* buf == "6.789us" */

diagnosticism_nanoseconds_to_string(sizeof(buf), buf, 999772000, DIAGNOSTICISM_NS_TO_STRING_F_PLUS, &n);
/* buf == "+999.7ms" */

Tracing (<diagnosticism/tracing.h>) since 0.1.0:

  • diagnosticism_trace_impl() - low-level implementation used by the diagnosticism_trace() macro; writes a trace line to a given FILE* stream, including file, line, and function, followed by a printf-style formatted message;

Version strings (<diagnosticism/version_string.h>) since 0.2.0:

  • diagnosticism_calc_version_string() - formats a version number (major, minor, patch, and optional alpha/beta/rc/build component) into a caller-supplied buffer; each component is an int interpreted strictly as an 8-bit value. Returns 0 on success (with character count via pnWritten), or -1 on failure. The verAlphaBeta argument encodes pre-release information: values in the ranges 0x40+, 0x80+, and 0xC0+ yield -alphaN, -betaN, and -rcN suffixes respectively (e.g. 0x42 → -alpha2, 0x83 → -beta3, 0xC1 → -rc1); 0xFF suppresses any suffix (final); other non-zero values are appended as a fourth dotted component. For example:
#include <diagnosticism/version_string.h>

char    buf[64];
size_t  n;

diagnosticism_calc_version_string(buf, sizeof(buf), 1, 2, 3, 0x42, &n);
/* buf == "1.2.3-alpha2" */

diagnosticism_calc_version_string(buf, sizeof(buf), 1, 2, 3, 0x83, &n);
/* buf == "1.2.3-beta3" */

diagnosticism_calc_version_string(buf, sizeof(buf), 1, 2, 3, 0xC1, &n);
/* buf == "1.2.3-rc1" */

diagnosticism_calc_version_string(buf, sizeof(buf), 1, 2, 3, 0xFF, &n);
/* buf == "1.2.3"  (final — no suffix) */

diagnosticism_calc_version_string(buf, sizeof(buf), 1, 2, 3, 0x05, &n);
/* buf == "1.2.3.5"  (fourth dotted component) */

See also examples/version_string/.

  • diagnosticism_calc_version_string_16() since 0.3.0 - as diagnosticism_calc_version_string(), but with uint16_t components and 16-bit pre-release encoding (0x4000+ / 0x8000+ / 0xC000+ / 0xFFFF);

Macros

The following macros are defined in the public C headers:

Core API (<diagnosticism/diagnosticism.h>):

  • DIAGNOSTICISM_CALL(rt) - expands to the appropriate linkage/return-type decoration for C and C++ (extern "C" rt in C++, rt in C);
  • DIAGNOSTICISM_CALL_NORETURN since 0.3.0 - as DIAGNOSTICISM_CALL(void), for functions that do not return (currently pending [[noreturn]] treatment);

DoomGram (<diagnosticism/doomgram.h>) since 0.1.0:

  • DIAGNOSTICISM_DOOMGRAM_INITIALIZER - brace initialiser for a zeroed diagnosticism_doomgram_t instance;
  • diagnosticism_doomgram_num_events_in_1ns() through diagnosticism_doomgram_num_events_ge_100s() - elicit the event count for each order-of-magnitude bucket;
  • diagnosticism_doomgram_try_get_total_event_time_ns_raw() - obtains the cumulative event time (in nanoseconds) without regard to overflow;

Nanosecond duration strings (<diagnosticism/nanoseconds_to_string.h>) since 0.4.0:

  • DIAGNOSTICISM_NS_TO_STRING_MIN_CCH - minimum buffer capacity (including NUL) always sufficient for diagnosticism_nanoseconds_to_string();
  • DIAGNOSTICISM_NS_TO_STRING_F_ASCII_ONLY - emit the microsecond unit as ASCII "us" instead of UTF-8 "µs";
  • DIAGNOSTICISM_NS_TO_STRING_F_NONE - no special formatting flags;
  • DIAGNOSTICISM_NS_TO_STRING_F_PLUS - request an explicit leading + on positive (non-zero) durations;

Tracing (<diagnosticism/tracing.h>) since 0.1.0:

  • diagnosticism_trace(stm, ...) - writes a trace line to stm at the call site, automatically supplying __FILE__, __LINE__, and the current function name together with a printf-style formatted message. For example:
#include <diagnosticism/tracing.h>

diagnosticism_trace(stderr, "argc=%d", argc);
diagnosticism_trace(stderr, "argument=%s", argument);

See the example program in examples/tracing/.

C++ API

The C++ API is intentionally small: a thin convenience layer over selected C functions. Include the C++ headers directly (they pull in the corresponding C headers).

Functions (C++)

Nanosecond duration strings (<diagnosticism/nanoseconds_to_string.hpp>) since 0.4.0:

  • diagnosticism::nanoseconds_to_string(int64_t, uint32_t = …) - returns a std::string; throws std::runtime_error on failure. For example:
#include <diagnosticism/nanoseconds_to_string.hpp>

std::string const s = diagnosticism::nanoseconds_to_string(123456789);
// s == "123.4ms"
  • diagnosticism::nanoseconds_to_string(T_string&, int64_t, uint32_t = …) - writes into a mutable string-like destination (assign(char const*, size_t)); returns a reference to that destination. For example:
#include <diagnosticism/nanoseconds_to_string.hpp>

std::string s;
diagnosticism::nanoseconds_to_string(s, 6789);
// s == "6.789µs"

Version strings (<diagnosticism/version_string.hpp>) since 0.2.0:

  • diagnosticism::calc_version_string() - C++ wrapper around diagnosticism_calc_version_string() that returns a std::string; throws std::runtime_error on failure. For example:
#include <diagnosticism/version_string.hpp>

std::string const alpha = diagnosticism::calc_version_string(1, 2, 3, 0x42);
// alpha == "1.2.3-alpha2"

std::string const beta = diagnosticism::calc_version_string(1, 2, 3, 0x83);
// beta == "1.2.3-beta3"

std::string const rc = diagnosticism::calc_version_string(1, 2, 3, 0xC1);
// rc == "1.2.3-rc1"

std::string const released = diagnosticism::calc_version_string(1, 2, 3, 0xFF);
// released == "1.2.3"  (final — no suffix)
  • diagnosticism::calc_version_string_16() since 0.3.0 - C++ wrapper around diagnosticism_calc_version_string_16();

Examples

Examples are provided in the examples directory, along with a markdown description for each. A detailed list is provided in EXAMPLES.md.

Example - DoomGram

The example program doomgram (in the examples directory) illustrates use of diagnosticism_doomgram_t to accumulate many timing events and obtain the 12-character timing strip. Each strip character corresponds to one order-of-magnitude bucket (from 1ns+ through 100s+). Within a bucket, _ means no events, a-z encodes increasing event counts, and * indicates overflow of the representable range.

The program records a spread of durations across the magnitude range, using {}-guarded loops where several events share the same order-of-magnitude. See examples/doomgram/main.c and examples/doomgram.md.

// examples/doomgram/main.c (extract)

diagnosticism_doomgram_push_event_time_ns(&dg,   9);
{ for (int i = 0; i != 25; ++i)
diagnosticism_doomgram_push_event_time_ns(&dg,  80);
}
{ for (int i = 0; i != 15; ++i)
diagnosticism_doomgram_push_event_time_ns(&dg, 700);
}

{ for (int i = 0; i != 30; ++i)
diagnosticism_doomgram_push_event_time_us(&dg,   6);
}
diagnosticism_doomgram_push_event_time_us(&dg,  50);
{ for (int i = 0; i != 120000; ++i)
diagnosticism_doomgram_push_event_time_us(&dg, 400);
}

/* diagnosticism_doomgram_push_event_time_ms(&dg,   3); */
{ for (int i = 0; i != 22; ++i)
diagnosticism_doomgram_push_event_time_ms(&dg,  20);
}
diagnosticism_doomgram_push_event_time_ms(&dg, 100);

diagnosticism_doomgram_push_event_time_s(&dg,    9);
/* ... further s-bucket pushes ... */

diagnosticism_doomgram_to_strip_12(&dg, (char (*)[12])strip);
strip[12] = '\0';

diagnosticism_doomgram_try_get_min_event_time_ns(&dg, &min_ns);
diagnosticism_doomgram_try_get_max_event_time_ns(&dg, &max_ns);

fprintf(stderr,
    "strip: %s; min: %" PRIu64 "ns;  max: %" PRIu64 "ns;  event_count: %" PRIu64 "\n",
    strip, min_ns, max_ns, dg.event_count);

Typical output is:

strip: abbbaf_baaaa; min: 9ns;  max: 700000000000ns;  event_count: 120106

indicating, among other buckets:

  • a in the 1ns+ bucket (1 event at 9ns);
  • b in the 10ns+ and 100ns+ buckets (25 events at 80ns; 15 at 700ns);
  • b in the 1µs+ bucket (30 events at 6µs);
  • f in the 100µs+ bucket (120000 events at 400µs);
  • _ in the 1ms+ bucket (the single 3ms push is commented out in the example source);
  • and further non-uniform counts across the remaining magnitude buckets.

In a live system one would log only the strip (and perhaps min/max), not the full internal histogram state.

Example - nanoseconds_to_string

The example program nanoseconds_to_string (in the examples directory) illustrates use of diagnosticism_nanoseconds_to_string() for compact duration formatting — including the default UTF-8 "µs" unit, DIAGNOSTICISM_NS_TO_STRING_F_ASCII_ONLY ("us"), DIAGNOSTICISM_NS_TO_STRING_F_PLUS, and a cchBuffer == 0 size probe. See examples/nanoseconds_to_string/main.c and examples/nanoseconds_to_string.md.

Example - tracing

The example program tracing (in the examples directory) illustrates use of diagnosticism_trace() at the call site. See examples/tracing/main.c and examples/tracing.md.

#include <diagnosticism/tracing.h>

diagnosticism_trace(stderr, "argc=%d, argv=...", argc);
diagnosticism_trace(stderr, "argument=%s", argument);

Typical output (to stderr) is along the lines of:

examples/tracing/main.c:22:main(argc=1, argv=...)
examples/tracing/main.c:10:f()
examples/tracing/main.c:15:g(argument=the argument to g)

Project Information

Where to get help

GitHub Page

Contribution guidelines

Defect reports, feature requests, and pull requests are welcome on https://github.com/synesissoftware/Diagnosticism.

Dependencies

Library use / install

Diagnosticism::core has no package dependencies. After install, consumer projects need only:

find_package(Diagnosticism REQUIRED)
target_link_libraries(myapp PRIVATE Diagnosticism::core)

Public headers do not include STLSoft (or any other Synesis library). Optional STLSOFT_CF_* guards in headers are inert unless a consumer already defines those macros.

Building automated tests

When configuring with BUILD_TESTING=ON (the default for prepare_cmake.sh), the following are required for the test suite only — not for building or installing the library:

  • STLSoft (1.11+ recommended) — provide via find_package(STLSoft), CMake variable STLSOFT, or environment variable STLSOFT (see prepare_cmake.sh --stlsoft-root-dir);
  • xTests (0.25+);

Optional for testing:

Pass --disable-testing / -T to prepare_cmake.sh (or -DBUILD_TESTING=OFF) to build and install without those projects.

Related projects

Other implementations in the Diagnosticism family:

The DoomGram component in this (C) library is a port of stlsoft::doomgram from STLSoft; the C implementation is self-contained and does not require STLSoft at build or use time.

License

Diagnosticism is released under the 3-clause BSD license. See LICENSE for details.