]> sigrok.org Git - libsigrok.git/blobdiff - src/input/vcd.c
output/csv: use intermediate time_t var, silence compiler warning
[libsigrok.git] / src / input / vcd.c
index 59015055e5e600c69deb4c6b6377d63308afddc8..d5471a47163236f239fb7bef16d5fc9abcdf620b 100644 (file)
  * - Check VCD input to VCD output behaviour. Verify that export and
  *   re-import results in identical data (well, VCD's constraints on
  *   timescale values is known to result in differences).
+ * - Check the minimum timestamp delta in the input data set, suggest
+ *   the downsample=N option to users for reduced resource consumption.
+ *   Popular VCD file creation utilities love to specify insanely tiny
+ *   timescale values in the pico or even femto seconds range. Which
+ *   results in huge sample counts after import, and potentially even
+ *   terminates the application due to resource exhaustion. This issue
+ *   only will vanish when common libsigrok infrastructure no longer
+ *   depends on constant rate streams of samples at discrete points
+ *   in time. The current input module implementation has code in place
+ *   to gather timestamp statistics, but the most appropriate condition
+ *   when to notify users is yet to be found.
  * - Cleanup the implementation.
  *   - Consistent use of the glib API (where appropriate).
  *   - More appropriate variable/function identifiers.
@@ -141,11 +152,18 @@ struct context {
        } conv_bits;
        GString *scope_prefix;
        struct feed_queue_logic *feed_logic;
-       struct split_state {
-               size_t alloced;
-               char **words;
-               gboolean in_use;
-       } split;
+       struct ts_stats {
+               size_t total_ts_seen;
+               uint64_t last_ts_value;
+               uint64_t last_ts_delta;
+               size_t min_count;
+               struct {
+                       uint64_t delta;
+                       size_t count;
+               } min_items[2];
+               uint32_t early_check_shift;
+               size_t early_last_emitted;
+       } ts_stats;
        struct vcd_prev {
                GSList *sr_channels;
                GSList *sr_groups;
@@ -182,13 +200,242 @@ static void free_channel(void *data)
        g_free(vcd_ch);
 }
 
-/* TODO Drop the local decl when this has become a common helper. */
-void sr_channel_group_free(struct sr_channel_group *cg);
+/*
+ * Another timestamp delta was observed, update statistics: Update the
+ * sorted list of minimum values, and increment the occurance counter.
+ * Returns the position of the item's statistics slot, or returns a huge
+ * invalid index when the current delta is larger than previously found
+ * values.
+ */
+static size_t ts_stats_update_min(struct ts_stats *stats, uint64_t delta)
+{
+       size_t idx, copy_idx;
+
+       /* Advance over previously recorded values which are smaller. */
+       idx = 0;
+       while (idx < stats->min_count && stats->min_items[idx].delta < delta)
+               idx++;
+       if (idx == ARRAY_SIZE(stats->min_items))
+               return idx;
+
+       /* Found the exact value that previously was registered? */
+       if (stats->min_items[idx].delta == delta) {
+               stats->min_items[idx].count++;
+               return idx;
+       }
+
+       /* Allocate another slot, bubble up larger values as needed. */
+       if (stats->min_count < ARRAY_SIZE(stats->min_items))
+               stats->min_count++;
+       for (copy_idx = stats->min_count - 1; copy_idx > idx; copy_idx--)
+               stats->min_items[copy_idx] = stats->min_items[copy_idx - 1];
+
+       /* Start tracking this value in the found or freed slot. */
+       memset(&stats->min_items[idx], 0, sizeof(stats->min_items[idx]));
+       stats->min_items[idx].delta = delta;
+       stats->min_items[idx].count++;
 
-/* Wrapper for GDestroyNotify compatibility. */
-static void cg_free(void *p)
+       return idx;
+}
+
+/*
+ * Intermediate check for extreme oversampling in the input data. Rate
+ * limited emission of warnings to avoid noise, "late" emission of the
+ * first potential message to avoid false positives, yet need to  emit
+ * the messages early (*way* before EOF) to raise awareness.
+ *
+ * TODO
+ * Tune the limits, improve perception and usefulness of these checks.
+ * Need to start emitting messages soon enough to be seen by users. Yet
+ * avoid unnecessary messages for valid input's idle/quiet phases. Slow
+ * input transitions are perfectly legal before bursty phases are seen
+ * in the input data. Needs the check become an option, on by default,
+ * but suppressable by users?
+ */
+static void ts_stats_check_early(struct ts_stats *stats)
 {
-       sr_channel_group_free(p);
+       static const struct {
+               uint64_t delta;
+               size_t count;
+       } *cp, check_points[] = {
+               {     100, 1000000, }, /* Still x100 after 1mio transitions. */
+               {    1000,  100000, }, /* Still x1k after 100k transitions. */
+               {   10000,   10000, }, /* Still x10k after 10k transitions. */
+               { 1000000,    2500, }, /* Still x1m after 2.5k transitions. */
+       };
+
+       size_t cp_idx;
+       uint64_t seen_delta, check_delta;
+       size_t seen_count;
+
+       /* Get the current minimum's value and count. */
+       if (!stats->min_count)
+               return;
+       seen_delta = stats->min_items[0].delta;
+       seen_count = stats->min_items[0].count;
+
+       /* Emit at most one weak message per import. */
+       if (stats->early_last_emitted)
+               return;
+
+       /* Check arbitrary marks, emit rate limited warnings. */
+       (void)seen_count;
+       check_delta = seen_delta >> stats->early_check_shift;
+       for (cp_idx = 0; cp_idx < ARRAY_SIZE(check_points); cp_idx++) {
+               cp = &check_points[cp_idx];
+               /* No other match can happen below. Done iterating. */
+               if (stats->total_ts_seen > cp->count)
+                       return;
+               /* Advance to the next checkpoint description. */
+               if (stats->total_ts_seen != cp->count)
+                       continue;
+               /* First occurance of that timestamp count. Check the value. */
+               sr_dbg("TS early chk: total %zu, min delta %" PRIu64 " / %" PRIu64 ".",
+                       cp->count, seen_delta, check_delta);
+               if (check_delta < cp->delta)
+                       return;
+               sr_warn("Low change rate? (weak estimate, min TS delta %" PRIu64 " after %zu timestamps)",
+                       seen_delta, stats->total_ts_seen);
+               sr_warn("Consider using the downsample=N option, or increasing its value.");
+               stats->early_last_emitted = stats->total_ts_seen;
+               return;
+       }
+}
+
+/* Reset the internal state of the timestamp tracker. */
+static int ts_stats_prep(struct context *inc)
+{
+       struct ts_stats *stats;
+       uint64_t down_sample_value;
+       uint32_t down_sample_shift;
+
+       stats = &inc->ts_stats;
+       memset(stats, 0, sizeof(*stats));
+
+       down_sample_value = inc->options.downsample;
+       down_sample_shift = 0;
+       while (down_sample_value >= 2) {
+               down_sample_shift++;
+               down_sample_value /= 2;
+       }
+       stats->early_check_shift = down_sample_shift;
+
+       return SR_OK;
+}
+
+/* Inspect another timestamp that was received. */
+static int ts_stats_check(struct ts_stats *stats, uint64_t curr_ts)
+{
+       uint64_t last_ts, delta;
+
+       last_ts = stats->last_ts_value;
+       stats->last_ts_value = curr_ts;
+       stats->total_ts_seen++;
+       if (stats->total_ts_seen < 2)
+               return SR_OK;
+
+       delta = curr_ts - last_ts;
+       stats->last_ts_delta = delta;
+       (void)ts_stats_update_min(stats, delta);
+
+       ts_stats_check_early(stats);
+
+       return SR_OK;
+}
+
+/* Postprocess internal timestamp tracker state. */
+static int ts_stats_post(struct context *inc, gboolean ignore_terminal)
+{
+       struct ts_stats *stats;
+       size_t min_idx;
+       uint64_t delta, over_sample, over_sample_scaled, suggest_factor;
+       enum sr_loglevel log_level;
+       gboolean is_suspicious, has_downsample;
+
+       stats = &inc->ts_stats;
+
+       /*
+        * Lookup the smallest timestamp delta which was found during
+        * data import. Ignore the last delta if its timestamp was never
+        * followed by data, and this was the only occurance. Absence of
+        * result data is non-fatal here -- this code exclusively serves
+        * to raise users' awareness of potential pitfalls, but does not
+        * change behaviour of data processing.
+        *
+        * TODO Also filter by occurance count? To not emit warnings when
+        * captured signals only change slowly by design. Only warn when
+        * the sample rate and samples count product exceeds a threshold?
+        * See below for the necessity (and potential) to adjust the log
+        * message's severity and content.
+        */
+       min_idx = 0;
+       if (ignore_terminal) do {
+               if (min_idx >= stats->min_count)
+                       break;
+               delta = stats->last_ts_delta;
+               if (stats->min_items[min_idx].delta != delta)
+                       break;
+               if (stats->min_items[min_idx].count != 1)
+                       break;
+               min_idx++;
+       } while (0);
+       if (min_idx >= stats->min_count)
+               return SR_OK;
+
+       /*
+        * TODO Refine the condition whether to notify the user, and
+        * which severity to use after having inspected all input data.
+        * Any detail could get involved which previously was gathered
+        * during data processing: total sample count, channel count
+        * including their data type and bits width, the oversampling
+        * factor (minimum observed "change rate"), or any combination
+        * thereof. The current check is rather simple (unconditional
+        * warning for ratios starting at 100, regardless of sample or
+        * channel count).
+        */
+       over_sample = stats->min_items[min_idx].delta;
+       over_sample_scaled = over_sample / inc->options.downsample;
+       sr_dbg("TS post stats: oversample unscaled %" PRIu64 ", scaled %" PRIu64,
+               over_sample, over_sample_scaled);
+       if (over_sample_scaled < 10) {
+               sr_dbg("TS post stats: Low oversampling ratio, good.");
+               return SR_OK;
+       }
+
+       /*
+        * Avoid constructing the message from several tiny pieces by
+        * design, because this would be hard on translators. Stick with
+        * complete sentences instead, and accept the redundancy in the
+        * user's interest.
+        */
+       log_level = (over_sample_scaled > 20) ? SR_LOG_WARN : SR_LOG_INFO;
+       is_suspicious = over_sample_scaled > 20;
+       if (is_suspicious) {
+               sr_log(log_level, LOG_PREFIX ": "
+                       "Suspiciously low overall change rate (total min TS delta %" PRIu64 ").",
+                       over_sample_scaled);
+       } else {
+               sr_log(log_level, LOG_PREFIX ": "
+                       "Low overall change rate (total min TS delta %" PRIu64 ").",
+                       over_sample_scaled);
+       }
+       has_downsample = inc->options.downsample > 1;
+       suggest_factor = inc->options.downsample;
+       while (over_sample_scaled >= 10) {
+               suggest_factor *= 10;
+               over_sample_scaled /= 10;
+       }
+       if (has_downsample) {
+               sr_log(log_level, LOG_PREFIX ": "
+                       "Suggest higher downsample value, like %" PRIu64 ".",
+                       suggest_factor);
+       } else {
+               sr_log(log_level, LOG_PREFIX ": "
+                       "Suggest to downsample, value like %" PRIu64 ".",
+                       suggest_factor);
+       }
+
+       return SR_OK;
 }
 
 static void check_remove_bom(GString *buf)
@@ -205,6 +452,11 @@ static void check_remove_bom(GString *buf)
 /*
  * Reads a single VCD section from input file and parses it to name/contents.
  * e.g. $timescale 1ps $end => "timescale" "1ps"
+ *
+ * The section (its content and its opening/closing markers) can span
+ * multiple text lines. This routine must not modify the caller's input
+ * buffer. Executes potentially multiple times on the same input data,
+ * and executes outside of the processing of the file's data section.
  */
 static gboolean parse_section(GString *buf, char **name, char **contents)
 {
@@ -269,142 +521,6 @@ static gboolean parse_section(GString *buf, char **name, char **contents)
        return status;
 }
 
-/*
- * The glib routine which splits an input text into a list of words also
- * "provides empty strings" which application code then needs to remove.
- * And copies of the input text get allocated for all words.
- *
- * The repeated memory allocation is acceptable for small workloads like
- * parsing the header sections. But the heavy lifting for sample data is
- * done by DIY code to speedup execution. The use of glib routines would
- * severely hurt throughput. Allocated memory gets re-used while a strict
- * ping-pong pattern is assumed (each text line of input data enters and
- * leaves in a strict symmetrical manner, due to the organization of the
- * receive() routine and parse calls).
- */
-
-/* Remove empty parts from an array returned by g_strsplit(). */
-static void remove_empty_parts(gchar **parts)
-{
-       gchar **src, **dest;
-
-       src = dest = parts;
-       while (*src) {
-               if (!**src) {
-                       g_free(*src);
-               } else {
-                       if (dest != src)
-                               *dest = *src;
-                       dest++;
-               }
-               src++;
-       }
-       *dest = NULL;
-}
-
-static char **split_text_line(struct context *inc, char *text, size_t *count)
-{
-       struct split_state *state;
-       size_t counted, alloced, wanted;
-       char **words, *p, **new_words;
-
-       state = &inc->split;
-
-       if (count)
-               *count = 0;
-
-       if (state->in_use) {
-               sr_dbg("coding error, split() called while \"in use\".");
-               return NULL;
-       }
-
-       /*
-        * Seed allocation when invoked for the first time. Assume
-        * simple logic data, start with a few words per line. Will
-        * automatically adjust with subsequent use.
-        */
-       if (!state->alloced) {
-               alloced = 20;
-               words = g_malloc(sizeof(words[0]) * alloced);
-               if (!words)
-                       return NULL;
-               state->alloced = alloced;
-               state->words = words;
-       }
-
-       /* Start with most recently allocated word list space. */
-       alloced = state->alloced;
-       words = state->words;
-       counted = 0;
-
-       /* As long as more input text remains ... */
-       p = text;
-       while (*p) {
-               /* Resize word list if needed. Just double the size. */
-               if (counted + 1 >= alloced) {
-                       wanted = 2 * alloced;
-                       new_words = g_realloc(words, sizeof(words[0]) * wanted);
-                       if (!new_words) {
-                               return NULL;
-                       }
-                       words = new_words;
-                       alloced = wanted;
-                       state->words = words;
-                       state->alloced = alloced;
-               }
-
-               /* Skip leading spaces. */
-               while (g_ascii_isspace(*p))
-                       p++;
-               if (!*p)
-                       break;
-
-               /* Add found word to word list. */
-               words[counted++] = p;
-
-               /* Find end of the word. Terminate loop upon EOS. */
-               while (*p && !g_ascii_isspace(*p))
-                       p++;
-               if (!*p)
-                       break;
-
-               /* More text follows. Terminate the word. */
-               *p++ = '\0';
-       }
-
-       /*
-        * NULL terminate the word list. Provide its length so that
-        * calling code need not re-iterate the list to get the count.
-        */
-       words[counted] = NULL;
-       if (count)
-               *count = counted;
-       state->in_use = TRUE;
-
-       return words;
-}
-
-static void free_text_split(struct context *inc, char **words)
-{
-       struct split_state *state;
-
-       state = &inc->split;
-
-       if (words && words != state->words) {
-               sr_dbg("coding error, free() arg differs from split() result.");
-       }
-
-       /* "Double free" finally releases the memory. */
-       if (!state->in_use) {
-               g_free(state->words);
-               state->words = NULL;
-               state->alloced = 0;
-       }
-
-       /* Mark as no longer in use. */
-       state->in_use = FALSE;
-}
-
 static gboolean have_header(GString *buf)
 {
        static const char *enddef_txt = "$enddefinitions";
@@ -418,7 +534,14 @@ static gboolean have_header(GString *buf)
                return FALSE;
        p += strlen(enddef_txt);
 
-       /* Search for end of section (content expected to be empty). */
+       /*
+        * Search for end of section (content expected to be empty).
+        * Uses DIY logic to scan for the literals' presence including
+        * empty space between keywords. MUST NOT modify the caller's
+        * input data, potentially executes several times on the same
+        * receive buffer, and executes outside of the processing the
+        * file's data section.
+        */
        p_stop = &buf->str[buf->len];
        p_stop -= strlen(end_txt);
        while (p < p_stop && g_ascii_isspace(*p))
@@ -481,8 +604,7 @@ static int parse_timescale(struct context *inc, char *contents)
  */
 static int parse_scope(struct context *inc, char *contents, gboolean is_up)
 {
-       char *sep_pos, *name_pos;
-       char **parts;
+       char *sep_pos, *name_pos, *type_pos;
        size_t length;
 
        /*
@@ -522,15 +644,17 @@ static int parse_scope(struct context *inc, char *contents, gboolean is_up)
         * was emitted by libsigrok's VCD output module.
         */
        sr_spew("$scope, got: \"%s\"", contents);
-       parts = g_strsplit_set(contents, " \r\n\t", 0);
-       remove_empty_parts(parts);
-       length = g_strv_length(parts);
-       if (length != 2) {
-               sr_err("Unsupported 'scope' syntax: %s", contents);
-               g_strfreev(parts);
+       type_pos = sr_text_next_word(contents, &contents);
+       if (!type_pos) {
+               sr_err("Cannot parse 'scope' directive");
+               return SR_ERR_DATA;
+       }
+       name_pos = sr_text_next_word(contents, &contents);
+       if (!name_pos || contents) {
+               sr_err("Cannot parse 'scope' directive");
                return SR_ERR_DATA;
        }
-       name_pos = parts[1];
+
        if (strcmp(name_pos, PACKAGE_NAME) == 0) {
                sr_info("Skipping scope with application's package name: %s",
                        name_pos);
@@ -542,7 +666,6 @@ static int parse_scope(struct context *inc, char *contents, gboolean is_up)
                g_string_append_printf(inc->scope_prefix,
                        "%s%c%c", name_pos, SCOPE_SEP, '\0');
        }
-       g_strfreev(parts);
        sr_dbg("$scope, prefix now: \"%s\"", inc->scope_prefix->str);
 
        return SR_OK;
@@ -556,10 +679,9 @@ static int parse_scope(struct context *inc, char *contents, gboolean is_up)
  */
 static int parse_header_var(struct context *inc, char *contents)
 {
-       char **parts;
-       size_t length;
        char *type, *size_txt, *id, *ref, *idx;
        gboolean is_reg, is_wire, is_real, is_int;
+       gboolean is_str;
        enum sr_channeltype ch_type;
        size_t size, next_size;
        struct vcd_channel *vcd_ch;
@@ -568,34 +690,36 @@ static int parse_header_var(struct context *inc, char *contents)
         * Format of $var or $reg header specs:
         * $var type size identifier reference [opt-index] $end
         */
-       parts = g_strsplit_set(contents, " \r\n\t", 0);
-       remove_empty_parts(parts);
-       length = g_strv_length(parts);
-       if (length != 4 && length != 5) {
+       type = sr_text_next_word(contents, &contents);
+       size_txt = sr_text_next_word(contents, &contents);
+       id = sr_text_next_word(contents, &contents);
+       ref = sr_text_next_word(contents, &contents);
+       idx = sr_text_next_word(contents, &contents);
+       if (idx && !*idx)
+               idx = NULL;
+       if (!type || !size_txt || !id || !ref || contents) {
                sr_warn("$var section should have 4 or 5 items");
-               g_strfreev(parts);
                return SR_ERR_DATA;
        }
 
-       type = parts[0];
-       size_txt = parts[1];
-       id = parts[2];
-       ref = parts[3];
-       idx = parts[4];
-       if (idx && !*idx)
-               idx = NULL;
        is_reg = g_strcmp0(type, "reg") == 0;
        is_wire = g_strcmp0(type, "wire") == 0;
        is_real = g_strcmp0(type, "real") == 0;
        is_int = g_strcmp0(type, "integer") == 0;
+       is_str = g_strcmp0(type, "string") == 0;
 
        if (is_reg || is_wire) {
                ch_type = SR_CHANNEL_LOGIC;
        } else if (is_real || is_int) {
                ch_type = SR_CHANNEL_ANALOG;
+       } else if (is_str) {
+               sr_warn("Skipping id %s, name '%s%s', unsupported type '%s'.",
+                       id, ref, idx ? idx : "", type);
+               inc->ignored_signals = g_slist_append(inc->ignored_signals,
+                       g_strdup(id));
+               return SR_OK;
        } else {
-               sr_info("Unsupported signal type: '%s'", type);
-               g_strfreev(parts);
+               sr_err("Unsupported signal type: '%s'", type);
                return SR_ERR_DATA;
        }
 
@@ -621,7 +745,6 @@ static int parse_header_var(struct context *inc, char *contents)
        }
        if (!size) {
                sr_warn("Unsupported signal size: '%s'", size_txt);
-               g_strfreev(parts);
                return SR_ERR_DATA;
        }
        if (inc->conv_bits.max_bits < size)
@@ -632,7 +755,6 @@ static int parse_header_var(struct context *inc, char *contents)
                        ref, idx ? idx : "", inc->options.maxchannels);
                inc->ignored_signals = g_slist_append(inc->ignored_signals,
                        g_strdup(id));
-               g_strfreev(parts);
                return SR_OK;
        }
 
@@ -661,7 +783,6 @@ static int parse_header_var(struct context *inc, char *contents)
                vcd_ch->type == SR_CHANNEL_ANALOG ? "A" : "L",
                vcd_ch->array_index);
        inc->channels = g_slist_append(inc->channels, vcd_ch);
-       g_strfreev(parts);
 
        return SR_OK;
 }
@@ -804,10 +925,8 @@ static void create_channels(const struct sr_input *in,
                if (vcd_ch->type != ch_type)
                        continue;
                cg = NULL;
-               if (vcd_ch->size != 1) {
-                       cg = g_malloc0(sizeof(*cg));
-                       cg->name = g_strdup(vcd_ch->name);
-               }
+               if (vcd_ch->size != 1)
+                       cg = sr_channel_group_new(sdi, vcd_ch->name, NULL);
                for (size_idx = 0; size_idx < vcd_ch->size; size_idx++) {
                        ch_name = get_channel_name(vcd_ch, size_idx);
                        sr_dbg("sigrok channel idx %zu, name %s, type %s, en %d.",
@@ -819,8 +938,6 @@ static void create_channels(const struct sr_input *in,
                        if (cg)
                                cg->channels = g_slist_append(cg->channels, ch);
                }
-               if (cg)
-                       sdi->channel_groups = g_slist_append(sdi->channel_groups, cg);
        }
 }
 
@@ -835,9 +952,11 @@ static void create_feeds(const struct sr_input *in)
        inc = in->priv;
 
        /* Create one feed for logic data. */
-       inc->unit_size = (inc->logic_count + 7) / 8;
-       inc->feed_logic = feed_queue_logic_alloc(in->sdi,
-               CHUNK_SIZE / inc->unit_size, inc->unit_size);
+       if (inc->logic_count) {
+               inc->unit_size = (inc->logic_count + 7) / 8;
+               inc->feed_logic = feed_queue_logic_alloc(in->sdi,
+                       CHUNK_SIZE / inc->unit_size, inc->unit_size);
+       }
 
        /* Create one feed per analog channel. */
        for (l = inc->channels; l; l = l->next) {
@@ -863,7 +982,7 @@ static void keep_header_for_reread(const struct sr_input *in)
 
        inc = in->priv;
 
-       g_slist_free_full(inc->prev.sr_groups, cg_free);
+       g_slist_free_full(inc->prev.sr_groups, sr_channel_group_free_cb);
        inc->prev.sr_groups = in->sdi->channel_groups;
        in->sdi->channel_groups = NULL;
 
@@ -902,7 +1021,7 @@ static gboolean check_header_in_reread(const struct sr_input *in)
                return FALSE;
        }
 
-       g_slist_free_full(in->sdi->channel_groups, cg_free);
+       g_slist_free_full(in->sdi->channel_groups, sr_channel_group_free_cb);
        in->sdi->channel_groups = inc->prev.sr_groups;
        inc->prev.sr_groups = NULL;
 
@@ -917,41 +1036,43 @@ static gboolean check_header_in_reread(const struct sr_input *in)
 static int parse_header(const struct sr_input *in, GString *buf)
 {
        struct context *inc;
-       gboolean status;
+       gboolean enddef_seen, header_valid;
        char *name, *contents;
        size_t size;
+       int ret;
 
        inc = in->priv;
 
        /* Parse sections until complete header was seen. */
-       status = FALSE;
+       enddef_seen = FALSE;
+       header_valid = TRUE;
        name = contents = NULL;
        inc->conv_bits.max_bits = 1;
        while (parse_section(buf, &name, &contents)) {
                sr_dbg("Section '%s', contents '%s'.", name, contents);
 
                if (g_strcmp0(name, "enddefinitions") == 0) {
-                       status = TRUE;
+                       enddef_seen = TRUE;
                        goto done_section;
                }
                if (g_strcmp0(name, "timescale") == 0) {
                        if (parse_timescale(inc, contents) != SR_OK)
-                               status = FALSE;
+                               header_valid = FALSE;
                        goto done_section;
                }
                if (g_strcmp0(name, "scope") == 0) {
                        if (parse_scope(inc, contents, FALSE) != SR_OK)
-                               status = FALSE;
+                               header_valid = FALSE;
                        goto done_section;
                }
                if (g_strcmp0(name, "upscope") == 0) {
                        if (parse_scope(inc, NULL, TRUE) != SR_OK)
-                               status = FALSE;
+                               header_valid = FALSE;
                        goto done_section;
                }
                if (g_strcmp0(name, "var") == 0) {
                        if (parse_header_var(inc, contents) != SR_OK)
-                               status = FALSE;
+                               header_valid = FALSE;
                        goto done_section;
                }
 
@@ -961,14 +1082,14 @@ done_section:
                g_free(contents);
                contents = NULL;
 
-               if (status)
+               if (enddef_seen)
                        break;
        }
        g_free(name);
        g_free(contents);
 
-       inc->got_header = status;
-       if (!status)
+       inc->got_header = enddef_seen && header_valid;
+       if (!inc->got_header)
                return SR_ERR_DATA;
 
        /* Create sigrok channels here, late, logic before analog. */
@@ -1008,6 +1129,10 @@ done_section:
        for (size = 0; size < inc->analog_count; size++)
                inc->current_floats[size] = 0.;
 
+       ret = ts_stats_prep(inc);
+       if (ret != SR_OK)
+               return ret;
+
        return SR_OK;
 }
 
@@ -1027,7 +1152,7 @@ static void add_samples(const struct sr_input *in, size_t count, gboolean flush)
        inc = in->priv;
 
        if (inc->logic_count) {
-               feed_queue_logic_submit(inc->feed_logic,
+               feed_queue_logic_submit_one(inc->feed_logic,
                        inc->current_logic, count);
                if (flush)
                        feed_queue_logic_flush(inc->feed_logic);
@@ -1040,7 +1165,7 @@ static void add_samples(const struct sr_input *in, size_t count, gboolean flush)
                if (!q)
                        continue;
                value = inc->current_floats[vcd_ch->array_index];
-               feed_queue_analog_submit(q, value, count);
+               feed_queue_analog_submit_one(q, value, count);
                if (flush)
                        feed_queue_analog_flush(q);
        }
@@ -1242,15 +1367,103 @@ static uint8_t vcd_char_to_value(char bit_char, int *warn)
        return ~0;
 }
 
+/*
+ * Check the validity of a VCD string value. It's essential to reliably
+ * accept valid data which the community uses in the field, yet robustly
+ * reject invalid data for users' awareness. Since IEEE 1800-2017 would
+ * not discuss the representation of this data type, it's assumed to not
+ * be an official feature of the VCD file format. This implementation is
+ * an educated guess after inspection of other arbitrary implementations,
+ * not backed by any specification or public documentation.
+ *
+ * A quick summary of the implemented assumptions: Must be a sequence of
+ * ASCII printables. Must not contain whitespace. Might contain escape
+ * sequences: A backslash followed by a single character, like '\n' or
+ * '\\'. Or a backslash and the letter x followed by two hex digits,
+ * like '\x20'. Or a backslash followed by three octal digits, like
+ * '\007'. As an exception also accepts a single digit '\0' but only at
+ * the text end. The string value may be empty, but must not be NULL.
+ *
+ * This implementation assumes an ASCII based platform for simplicity
+ * and readability. Should be a given on sigrok supported platforms.
+ */
+static gboolean vcd_string_valid(const char *s)
+{
+       char c;
+
+       if (!s)
+               return FALSE;
+
+       while (*s) {
+               c = *s++;
+               /* Reject non-printable ASCII chars including DEL. */
+               if (c < ' ')
+                       return FALSE;
+               if (c > '~')
+                       return FALSE;
+               /* Deeper inspection of escape sequences. */
+               if (c == '\\') {
+                       c = *s++;
+                       switch (c) {
+                       case 'a': /* BEL, bell aka "alarm" */
+                       case 'b': /* BS, back space */
+                       case 't': /* TAB, tabulator */
+                       case 'n': /* NL, newline */
+                       case 'v': /* VT, vertical tabulator */
+                       case 'f': /* FF, form feed */
+                       case 'r': /* CR, carriage return */
+                       case '"': /* double quotes */
+                       case '\'': /* tick, single quote */
+                       case '?': /* question mark */
+                       case '\\': /* backslash */
+                               continue;
+                       case 'x': /* \xNN two hex digits */
+                               c = *s++;
+                               if (!g_ascii_isxdigit(c))
+                                       return FALSE;
+                               c = *s++;
+                               if (!g_ascii_isxdigit(c))
+                                       return FALSE;
+                               continue;
+                       case '0': /* \NNN three octal digits */
+                       case '1':
+                       case '2':
+                       case '3':
+                       case '4':
+                       case '5':
+                       case '6':
+                       case '7':
+                               /* Special case '\0' at end of text. */
+                               if (c == '0' && !*s)
+                                       return TRUE;
+                               /*
+                                * First digit was covered by the outer
+                                * switch(). Two more digits to check.
+                                */
+                               c = *s++;
+                               if (!g_ascii_isdigit(c) || c > '7')
+                                       return FALSE;
+                               c = *s++;
+                               if (!g_ascii_isdigit(c) || c > '7')
+                                       return FALSE;
+                               continue;
+                       default:
+                               return FALSE;
+                       }
+               }
+       }
+
+       return TRUE;
+}
+
 /* Parse one text line of the data section. */
-static int parse_textline(const struct sr_input *in, char *lines)
+static int parse_textline(const struct sr_input *in, char *line)
 {
        struct context *inc;
        int ret;
-       char **words;
-       size_t word_count, word_idx;
-       char *curr_word, *next_word, curr_first;
-       gboolean is_timestamp, is_section, is_real, is_multibit, is_singlebit;
+       char *curr_word, curr_first;
+       gboolean is_timestamp, is_section;
+       gboolean is_real, is_multibit, is_singlebit, is_string;
        uint64_t timestamp;
        char *identifier, *endptr;
        size_t count;
@@ -1258,30 +1471,33 @@ static int parse_textline(const struct sr_input *in, char *lines)
        inc = in->priv;
 
        /*
-        * Split the caller's text lines into a list of space separated
-        * words. Note that some of the branches consume the very next
-        * words as well, and assume that both adjacent words will be
-        * available when the first word is seen. This constraint applies
-        * to bit vector data, multi-bit integers and real (float) data,
-        * as well as single-bit data with whitespace before its
-        * identifier (if that's valid in VCD, we'd accept it here).
+        * Consume space separated words from a caller's text line. Note
+        * that many words are self contained, but some require another
+        * word to follow. This implementation assumes that both words
+        * (when involved) become available in the same invocation, that
+        * is that both words reside on the same text line of the file.
         * The fact that callers always pass complete text lines should
-        * make this assumption acceptable.
+        * make this assumption acceptable. No generator is known to
+        * split two corresponding words across text lines.
+        *
+        * This constraint applies to bit vector data, multi-bit integer
+        * and real (float) values, text strings, as well as single-bit
+        * values with whitespace before their identifiers (if that is
+        * valid in VCD, we'd accept it here; if generators don't create
+        * such input, then support for it does not harm).
         */
        ret = SR_OK;
-       words = split_text_line(inc, lines, &word_count);
-       for (word_idx = 0; word_idx < word_count; word_idx++) {
+       while (line) {
                /*
-                * Make the next two words available, to simpilify code
-                * paths below. The second word is optional here.
+                * Lookup one word here which is mandatory. Locations
+                * below conditionally lookup another word as needed.
                 */
-               curr_word = words[word_idx];
-               if (!curr_word && !curr_word[0])
+               curr_word = sr_text_next_word(line, &line);
+               if (!curr_word)
+                       break;
+               if (!*curr_word)
                        continue;
                curr_first = g_ascii_tolower(curr_word[0]);
-               next_word = words[word_idx + 1];
-               if (next_word && !next_word[0])
-                       next_word = NULL;
 
                /*
                 * Optionally skip some sections that can be interleaved
@@ -1354,6 +1570,9 @@ static int parse_textline(const struct sr_input *in, char *lines)
                                break;
                        }
                        sr_spew("Got timestamp: %" PRIu64, timestamp);
+                       ret = ts_stats_check(&inc->ts_stats, timestamp);
+                       if (ret != SR_OK)
+                               break;
                        if (inc->options.downsample > 1) {
                                timestamp /= inc->options.downsample;
                                sr_spew("Downsampled timestamp: %" PRIu64, timestamp);
@@ -1426,6 +1645,7 @@ static int parse_textline(const struct sr_input *in, char *lines)
                 * timestamp.
                 *
                 * Supported input data formats are:
+                * - S<value> <sep> <id> (value not used, VCD type 'string').
                 * - R<value> <sep> <id> (analog channel, VCD type 'real').
                 * - B<value> <sep> <id> (analog channel, VCD type 'integer').
                 * - B<value> <sep> <id> (logic channels, VCD bit vectors).
@@ -1454,13 +1674,13 @@ static int parse_textline(const struct sr_input *in, char *lines)
                is_singlebit |= curr_first == 'l' || curr_first == 'h';
                is_singlebit |= curr_first == 'x' || curr_first == 'z';
                is_singlebit |= curr_first == 'u' || curr_first == '-';
+               is_string = curr_first == 's';
                if (is_real) {
                        char *real_text;
                        float real_val;
 
                        real_text = &curr_word[1];
-                       identifier = next_word;
-                       word_idx++;
+                       identifier = sr_text_next_word(line, &line);
                        if (!*real_text || !identifier || !*identifier) {
                                sr_err("Unexpected real format.");
                                ret = SR_ERR_DATA;
@@ -1494,8 +1714,7 @@ static int parse_textline(const struct sr_input *in, char *lines)
                         * we may never unify code paths at all here.
                         */
                        bits_text = &curr_word[1];
-                       identifier = next_word;
-                       word_idx++;
+                       identifier = sr_text_next_word(line, &line);
 
                        if (!*bits_text || !identifier || !*identifier) {
                                sr_err("Unexpected integer/vector format.");
@@ -1580,10 +1799,8 @@ static int parse_textline(const struct sr_input *in, char *lines)
                                break;
                        }
                        identifier = ++bits_text;
-                       if (!*identifier) {
-                               identifier = next_word;
-                               word_idx++;
-                       }
+                       if (!*identifier)
+                               identifier = sr_text_next_word(line, &line);
                        if (!identifier || !*identifier) {
                                sr_err("Identifier missing.");
                                ret = SR_ERR_DATA;
@@ -1601,13 +1818,37 @@ static int parse_textline(const struct sr_input *in, char *lines)
                        process_bits(inc, identifier, inc->conv_bits.value, 1);
                        continue;
                }
+               if (is_string) {
+                       const char *str_value;
+
+                       str_value = &curr_word[1];
+                       identifier = sr_text_next_word(line, &line);
+                       if (!vcd_string_valid(str_value)) {
+                               sr_err("Invalid string data: %s", str_value);
+                               ret = SR_ERR_DATA;
+                               break;
+                       }
+                       if (!identifier || !*identifier) {
+                               sr_err("String value without identifier.");
+                               ret = SR_ERR_DATA;
+                               break;
+                       }
+                       sr_spew("Got string data, id '%s', value \"%s\".",
+                               identifier, str_value);
+                       if (!is_ignored(inc, identifier)) {
+                               sr_err("String value for identifier '%s'.",
+                                       identifier);
+                               ret = SR_ERR_DATA;
+                               break;
+                       }
+                       continue;
+               }
 
                /* Design choice: Consider unsupported input fatal. */
                sr_err("Unknown token '%s'.", curr_word);
                ret = SR_ERR_DATA;
                break;
        }
-       free_text_split(inc, words);
 
        return ret;
 }
@@ -1618,11 +1859,14 @@ static int process_buffer(struct sr_input *in, gboolean is_eof)
        uint64_t samplerate;
        GVariant *gvar;
        int ret;
-       char *rdptr, *endptr, *trimptr;
-       size_t rdlen;
+       char *rdptr, *line;
+       size_t taken, rdlen;
 
        inc = in->priv;
 
+       if (!inc->got_header)
+               return SR_ERR_DATA;
+
        /* Send feed header and samplerate (once) before sample data. */
        if (!inc->started) {
                std_session_send_df_header(in->sdi);
@@ -1647,28 +1891,19 @@ static int process_buffer(struct sr_input *in, gboolean is_eof)
        /* Find and process complete text lines in the input data. */
        ret = SR_OK;
        rdptr = in->buf->str;
-       while (TRUE) {
+       taken = 0;
+       while (rdptr) {
                rdlen = &in->buf->str[in->buf->len] - rdptr;
-               endptr = g_strstr_len(rdptr, rdlen, "\n");
-               if (!endptr)
+               line = sr_text_next_line(rdptr, rdlen, &rdptr, &taken);
+               if (!line)
                        break;
-               trimptr = endptr;
-               *endptr++ = '\0';
-               while (g_ascii_isspace(*rdptr))
-                       rdptr++;
-               while (trimptr > rdptr && g_ascii_isspace(trimptr[-1]))
-                       *(--trimptr) = '\0';
-               if (!*rdptr) {
-                       rdptr = endptr;
+               if (!*line)
                        continue;
-               }
-               ret = parse_textline(in, rdptr);
-               rdptr = endptr;
+               ret = parse_textline(in, line);
                if (ret != SR_OK)
                        break;
        }
-       rdlen = rdptr - in->buf->str;
-       g_string_erase(in->buf, 0, rdlen);
+       g_string_erase(in->buf, 0, taken);
 
        return ret;
 }
@@ -1783,8 +2018,14 @@ static int end(struct sr_input *in)
                ret = SR_OK;
 
        /* Flush most recently queued sample data when EOF is seen. */
-       count = inc->data_after_timestamp ? 1 : 0;
-       add_samples(in, count, TRUE);
+       if (inc->got_header && ret == SR_OK) {
+               count = inc->data_after_timestamp ? 1 : 0;
+               add_samples(in, count, TRUE);
+       }
+
+       /* Optionally suggest downsampling after all input data was seen. */
+       if (inc->got_header)
+               (void)ts_stats_post(inc, !inc->data_after_timestamp);
 
        /* Must send DF_END when DF_HEADER was sent before. */
        if (inc->started)
@@ -1815,7 +2056,6 @@ static void cleanup(struct sr_input *in)
        inc->scope_prefix = NULL;
        g_slist_free_full(inc->ignored_signals, g_free);
        inc->ignored_signals = NULL;
-       free_text_split(inc, NULL);
 }
 
 static int reset(struct sr_input *in)